多线程概念
- 线程
线程指的是:1个CPU执行的CPU命令列为一条无分叉路径 - 多线程
这种无分叉路径不止一条,存在多条即为"多线程".在多线程中,1个CPU核执行多条不同路径上的不同命令.需要明确的是:不管CPU技术如何,基本上1个CPU核一次能够执行的CPU命令始终为1.使用多线程的程序可以在某一个线程和其他线程之间反复进行上下文切换,因此,看上去好像1个CPU能够并列的执行多个线程一样,而在具有多个CPU核的情况下,就能够提供多个CPU核并行执行多个线程的技术.
- 多线程的缺点
多线程技术看起来非常美好,但实际上因为涉及到上下文切换,多线程执行的效率未必比单线程快,甚至可能会慢过单线程,而且,多个线程更新相同资源会导致数据的不一致(数据竞争),停止等待事件的线程会导致多个线程相互持续等待(死锁),使用太多线程会消耗掉大量内存等问题. - 多线程的优点
尽管极易发生各种问题,在iOS中也应当使用多线程编程.因为多线程编程可以保证应用程序的响应性能.
应用程序在启动时,通过最先执行额线程("主线程")来描绘用户界面,处理屏幕的事件.如果在该线程中进行长时间的处理,如数据库访问,网络请求等,就会妨碍主线程的执行,从而导致不能更新用户界面,应用程序的画面长时间停滞的问题.
3.为什么要使用多线程
使用多线程编程,在执行长时间的处理时,仍可保证用户界面的响应性能.这是我们使用多线程编程的最大好处,而且,苹果为了简化多线程的使用,给我们提供了多种多线程技术,本文主要介绍GCD结合NSoperation在开发中的使用.
GCD的API
- Dispatch Queue
Dispatch Queue 是执行处理的等待队列.,应用程序编程人员通过dispatch_async等API,在block中将要执行的处理追加到Dispatch Queue 中,Dispatch Queue 按照追加的顺序(FIFO,先进先出)执行处理.
Dispatch Queue 分为两种类型
- Serial Dispatch Queue 等待现在执行中处理结束
- Concurrent Dispatch Queue 不等待现在执行中处理结束
<pre>
-(void)serialDispatchQueue{
//serial Dispatch queue 的创建
dispatch_queue_t queue = dispatch_queue_create("DC.test01", NULL);
for (int i = 0; i< 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%d\n",i);
});
}
} - (void)concurrentDispatchQueue{
//Concurrent Dispatch queue 的创建
dispatch_queue_t queue = dispatch_queue_create("DC.test01", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i< 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%d\n",i);
});
}
}
</pre>
上面的代码中,分别创建了serial Dispatch Queue 和Concurrent Dispatch Queue.当为serial Dispatch Queu 时,因为要等待现在执行的处理结束,所以首先执行第一个任务,打印0,然后顺序依次执行其他任务(这里表现为0,1,2,3,4,5,6,7,8,9由小到大按顺序打印).系统只会使用一个线程.当为Concurrent Dispatch Queu 时,因为不用等待现在执行的处理结束,所以首先执行第一个任务,不管第一个任务执行是否结束,都开始执行第二个任务,不管第二个任务执行是否结束,都开始执行第三个任务,如此重复循环.(这里表现为0,1,2,3,4,5,6,7,8,9的打印没有按照有小到大的顺序,是一个随机顺序).系统可以并行执行多个处理,但是并行执行处理数量取决于当前系统的状态,即iOS基于Dispatch Queue 中的处理数,CPU核数以及CPU负荷等当前系统状态来决定Concurrent Dispatch Queue中并行执行的处理数.
当生成多个serial Dispatch Queue,各个serial Dispatch Queue 将并行执行,虽然在一个Serial Dispatch queue 中同时只能后执行一个追加处理,但是如果将处理分别追加到4个serial Dispatch queue 中,各个serial Dispatch queue 执行1个,即为同时执行4个处理.如果生成2000个serial Dispatch queue ,那么久生成2000个线程,而不像Concurrent Dispatch queue 那样,系统会根据系统状态来决定执行处理数(生成线程的个数).如果过多使用线程,就会消耗大量内存,引起大量的上下文切换,大幅降低系统的响应性能.因此,只在为了避免多线程变成问题之一---多个线程更新相同资源导致数据竞争时使用serial Dispatch queue.
- 除了使用了dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)这个方法去创建Dispatch Queue .实际上,不用特意生成Dispatch Queue ,我们也可以获取系统提供标准的Dispatch queue.那就是 Main Dispatch Queue 和Global Dispatch queue .
- Main Dispatch Queue 实在主线程中执行的Dispatch queue ,因为主线程只有一个,所以他自然是serial Dispatch queue ,追加到main Dispatch queue 的处理在主线程的RunLoop中执行.
- Global Dispatch queue 是所有的应用程序都能够使用的Concurrent Dispatch Queue .没有必要通过Dispatch_queue_creat 函数来生成.另外,Global Dispatch queue 有四个执行优先级,分别是高优先级(High Priority),默认优先级(Default Priority),低优先级(Low Priority)和后台优先级(Background Priority).
<pre>
//获取系统提供标准的Dispatch queue.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t globalDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t globalLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t globalbackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
</pre>
Dispatch_queue_create 函数生成的Dispatch queue 不管是serial 还是Concurrent,都使用与默认优先级Global Dispatch Queue 相同有限优先级的线程,可以使用Dispatch_set_target_queue函数变更Dispatch queue 的优先级.
2.Dispatch Group
在追加到Dispatch Queue 中的多个处理全部结束后想执行结束处理,开发中经常会碰到这种需求.当只是用一个serial Dispatch queue 的时候,只要将想执行的结果全部追加到serial Dispatch queue 中并在最后追加结束处理即可.但是在使用Concurrent Dispatch queue 或同时使用多个Dispatch queue 是.Dispatch Group就派上用场了.
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"complete");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blko");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk3");
});
//打印结果为 blk2 blk1 blko blk3
2016-04-23 15:00:00.260 test01[1687:171508] complete
</pre>
上面这段代码展示了Dispatch Group的用法.Dispatch Group 可以监视追到到Dispatch queue 中的处理的完成情况,一旦监测到所有处理执行结束,就将结束的处理追加到 dispatch_group_notify中指定的Dispatch queue 中执行.
除了使用dispatch_group_async 追加处理到Dispatch queue中,还有另外一函数:Dispatch_group_enter() 和Dispatch_group_leave().
2016.7.11更新:使用Dispatch_group_enter() 和Dispatch_group_leave()可以对网络请求等异步执行线程也执行回调监听
3.dispatch_barrier_async
在访问数据库或文件时,使用serial Dispatch queue 可以避免数据竞争问题.写入处理确实不可与其他的写入处理以及包含读取的其他某些处理并行执行,但是如果读取处理只是与读取处理并行执行,那么多个并行执行处理就不会发生问题.也就是说,为了高效率的访问,读取处理追加到Concurrent Dispatch queue 中,写入处理在任一个读取处理都没有执行的状态下,追加到serial Dispatch queue中即可.用之前的几个接口也可以实现这个功能,但是苹果系统了一个非常方便解决这个问题的接口:dispatch_barrier_async.用代码来演示dispatch_barrier_async的使用.
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{ NSLog(@"reading1"); });
dispatch_async(queue, ^{ NSLog(@"reading2"); });
dispatch_async(queue, ^{ NSLog(@"reading3"); });
dispatch_async(queue, ^{ NSLog(@"reading4"); });
dispatch_barrier_async(queue, ^{ NSLog(@"writing1"); });
dispatch_async(queue, ^{ NSLog(@"reading5"); });
dispatch_async(queue, ^{ NSLog(@"reading6"); });
dispatch_async(queue, ^{ NSLog(@"reading7"); });
dispatch_async(queue, ^{ NSLog(@"reading8"); });
</pre>
上面的代码打印结果为:2016-04-23 15:21:20.474 test01[1782:184574] reading1
2016-04-23 15:21:20.474 test01[1782:184575] reading2 reading3 reading4
2016-04-23 15:21:20.475 test01[1782:184635] writing1
2016-04-23 15:21:20.475 test01[1782:184574] reading5 reading6 reading7 reading8
Dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue 上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue 中,然后等待由Dispatch_barrier_async 追加的处理结束后,Concurrent Dispatch Queue才恢复为一般的动作.用下图来表示更加明了.将Concurrent Dispatch Queue分为三段.使用Concurrent Dispatch Queue 和Dispatch_barrier_async可以实现高效的函数库访问和文件访问.
4.dispatch_sync
Dispatch_async 函数的async意味着非同步,就是将指定的Block非同步的追加到指定的Dispatch queue中,Dispatch_async函数不做任何等待.
Dispatch_sync 函数的sync意味着同步,就是将指定的Block同步的追加到指定的Dispatch queue中,在追加的Block结束前,Dispatch_sync函数会一直等待.
等待意味着当前线程停止,开发中一定要非常注意这种情况(容易引起死锁).Dispatch_sync其实可以看做简易的Dispatch_group_wait函数.一旦调用Dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回.
<pre>
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"Hello 1");
dispatch_sync(queue, ^{
NSLog(@"Hello 2");
});
});
dispatch_sync(queue, ^{
NSLog(@"Hello 1");
});
</pre>
分析以上代码,main Dispatch Queue 中执行的Block 等待 main Dispatch Queue中要执行的Block
执行结束.引起死锁.
5.Dispatch Semaphore
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时程序还会异常结束.虽然使用serial Dispatch queue和Dispatch_barrier_async 函数可以避免这类问题,但是当需要进行更细粒度的排他控制时.我们就需要用到Dispatch semaphone了.
Dispatch semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号.在Dispatch Semaphore中.使用计数类实现该功能,计数为0时等待,计数为1或者大于1时,减去1而不等待.比较两段代码:
-
代码一:
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
运行后报错:test01(2034,0x10c403000) malloc: *** error for object 0x7fc1e263cbb8: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
(lldb)
</pre>
该代码使用global Dispatch queue 更新NSMutableArray,所以执行后,有内存错误导致程序异常结束的概率很高.
-
代码二:
<pre>
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//生成Dispatch Semaphore ,计数初始值设定为1,保证可访问NSMutableArray类对象的线程同时只能有1个
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{//等待Dispatch Semaphore ,一直等待.直到Dispatch Semaphore的计数值达到或者大于1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //由于Dispatch Semaphore的计数值大于或等于1,Dispatch Semaphore的计数值减1,dispatch_semaphore_wait执行返回. //此时Dispatch Semaphore的计数值等于0.由于可访问NSMutableArray类对象的线程同时只能有1个,因此可安全的进行更新 [array addObject:[NSNumber numberWithInt:i]]; //排他控制处理结束,Dispatch Semaphore的计数值加1 dispatch_semaphore_signal(semaphore); });
}
NSLog(@"%lu",(unsigned long)array.count);
打印结果test01[2045:222660] 99999,更新成功
</pre>
也可以参考同步块(synchronization block) 和NSLock的使用.
5.Dispatch_after
有时候会有这种情况,想在指定的时间后执行处理.这时候可以考虑使用Dispatch_after.需要注意的是,Dispatch_after并不是在指定的时间后执行处理,而只是在指定的时间追加处理到Dispatch queue.因为Mian Dispatch queue在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,Block最快3秒后执行,最慢在3+1/60后执行.虽然在有严格时间的要求下使用Dispatch_after会出问题,但在想大致延迟执行处理时可以使用.
6.Dispatch_once
使用Dispatch_once来执行只需要运行一次的线程安全代码,即单例模式.常用的写法如下:
<pre>
-
(instancetype)shareInstance{
static DCtest *shareInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[self alloc] init];
});
return shareInstance;
}
</pre>
NSOperationQueue
GCD技术确实非常棒,然而还有一种技术"NSOperationQueue",在某些情况下,使用NSOperationQueue比GCD更方便,我们也应该熟悉了解.
- 取消某个操作
在GCD中只负责往队列中添加任务,无法取消.然而如果使用操作队列.运行任务之前, 可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表示此任务不需执行,不过,已启动的任务无法取消. - 指定操作间的依赖关系
一个操作可以依赖其他多个操作.开发者能指定操作之间的依赖体系,是指定的操作必须在另外一个操 作顺利执行完毕后方可执行. - 指定操作的优先级
GCD也有优先级,不过只能指定队列的优先级,而不能指定某个操作的优先级. - 通知键值观测机制监控NSOperation对象的属性
NSOperation对象有许多属性都适合通过键值观测机制(KVO)来监听,.比如可以通过isCancelled属性来判断任务是否已取消,也可以通过isFinished来判断任务是否已完成.如果想在某个任务变更起状态是得到通知,那么键值观测很有用.
在多线程开发中,我们可以结合GCD和NSOperation,来更高效的实现多线程编程.