iOS------GCD一些概念的整理
在iOS开发中,经常会使用到GCD多线程编程的技术,在这里我们不多介绍GCD的具体使用,只是简单的聊一下GCD相关的一些非主流问题。
首先我们从一个谈到烂的话题开始:
死锁
什么是死锁呢?
所谓死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
这是百度对死锁的解释。上面说的是进程,当然对于线程来说,同样适用。
<strong>但是我想说的是GCD中出现了死锁,不同于上述概念的死锁</strong>,我们来看一下GCD中死锁的形成:
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_sync(main_queue, ^{
NSLog(@"主队列同步执行");
});
如果项目中出现这样的代码,肯定会出现死锁现象。上述代码的意思是:将任务同步提交至主队列执行,主队列需要停下目前正在执行的任务,转而执行同步提交的任务。但是因为主队列是一个串行队列,同时执行其他任务。其实除了主队了,任何串行队列都不能同步执行其他任务:
dispatch_queue_t queue = dispatch_queue_create("ljt", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"在queue队列同步执行");
});
});
我们新创建一个串行队列,然后将串行队列异步提交至主队列,然后在新创建的队列中,同步提交任务,这个时候还是会发生死锁。
<strong>我们可以得出一个结论,使用dispatch_sync函数同步提交任务时,如果这句的上下文和要提交的目标队列是同一个队列并且是串行队列的话,肯定会死锁的。而这种死锁是一种线程调度死锁</strong> 注意队列是串行队列,如果是并行队列的话,是没有问题的。
</br>具体的原因我们稍后分析,现在先看一看另一个问题:
队列 线程 RunLoop的关系
队列: 队列其实是一个任务调度系统,负责调度一个个线程执行具体的任务,可以把它理解为线程的容器,队列分为串行队列和并行队列,串行队列同时只能调度一个唯一的线程,而并行队列同时可以调度多个线程。 线程 + 任务的容器 = 队列
线程: 具体执行任务的过程,
RunLoop: 每一个线程都有一个RunLoop,关于RunLoop的具体介绍请自行谷歌百度,其实RunLoop就是一个线程的保活机制,先需要的时候调起线程(无需重新创建),不需要的时候使线程休眠,以节省资源。
综上:GCD是一个任务调度系统,负责将我们提交的任务分配到不同的线程上去执行,我们提交任务的方式有同步和异步。
同步提交:就是停下目前正在执行的任务,等待所提交的任务执行完毕后再继续执行,会阻塞当前任务的执行
异步提交:只管向队列中提交任务,不用等待所提交的任务执行完毕,所提交的任务具体什么时候执行,这个要看当前的系统的队列状态。不会阻塞当前任务的执行。
<strong>注意等待这个词</strong>
而GCD得队列又分为串行队列和并行队列,所以我们就可以得到四种不同的任务执行方式:
串行同步提交:
串行异步提交:
并行同步提交:
并行异步提交:
现在我们就来分析一下最开始所出现的死锁现象,我们知道,死锁状态是发生在第一种任务执行方式中:
<strong>串行同步提交</strong> ,为什么会出现这个问题呢?
举个例子: 这个例子也许不太合适,但是实在找不出更加合适的例子了。。
我们把队列看做是厨师长,具体执行的任务的线程可以看做是一个个的小厨,
串行队列呢,厨师长同一时刻只允许调度一个小厨来工作,而这个小厨不能随时切换当前所处理任务(因为串行嘛),当我们同步提交任务的时候,根据定义,我们需要停下当前的任务,立即执行我们所提交的任务,等待执行完所提交的任务后继续执行之前暂停的任务。但是串行队列(厨师长)同一时刻只有一个小厨可供调用,当同步提交任务的时候,就相当于说,小厨你不要放下现在手头的工作,同时要把新分配的任务执行完,这个时候小厨就为难了,我分身乏术啊,臣妾做不到啊。这就形成了队列调度资源的竞争,出现了死锁现象。
其他的几种情况为什么没有问题呢?
<strong>串行异步提交</strong> 异步,就是说,不用等待当前任务执行完毕。用上面的例子解释:虽然厨师长,同一时刻只有一个小厨可供调用,但是因为是异步提交,当接到任务交给小厨后,说这个任务需要执行(不必同时立马),小厨可以等到手头的工作做完之后再来执行,完全没问题。这个时候其实只有一个小厨在工作,也就是实际上只有一个线程执行</br>
<strong>并行同步提交</strong> 并行队列,这个就更没问题了,并行队列相当于说,厨师长可以有很多小厨可供调用,具体有多少小厨不太清楚,但是肯定够用,并且小厨可以随时切换当前的所正在执行的任务(因为并行嘛),当同步提交的时候,厨师长要求这个小厨立马切换工作状态,去执行新提交分配的任务,执行完之后在回头执行之前的任务,这个时候其实只有一个小厨在工作,也就是实际上只有一个线程执行</br>
<strong>并行异步提交</strong> 这种情况下,就是一个厨师长可以调度多个小厨,厨师长只管把任务分配给不同的小厨,不用等待提交的任务立即执行完成,小厨具体什么时候做完,厨师长不关心。
何时创建新的线程
1、队列刚启动的时候
当我们创建队列的时候,这个队列是空的,当我们向队列中提交任务的时候才会创建新的线程。
2、串行队列异步执行
dispatch_queue_t queue = dispatch_queue_create("ljt", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步提交任务1---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(2);
NSLog(@"异步提交任务2---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"异步提交任务3---%@",[NSThread currentThread]);
});
NSLog(@"队列执行完毕");
});
执行结果:
2017-05-02 13:22:34.539 TestGCD[1490:58336] <NSThread: 0x600000071e40>{number = 1, name = main}
2017-05-02 13:22:34.539 TestGCD[1490:58336] 队列执行完毕
2017-05-02 13:22:37.585 TestGCD[1490:58381] 异步提交任务1---<NSThread: 0x608000074340>{number = 3, name = (null)}
2017-05-02 13:22:39.586 TestGCD[1490:58381] 异步提交任务2---<NSThread: 0x608000074340>{number = 3, name = (null)}
2017-05-02 13:22:40.653 TestGCD[1490:58381] 异步提交任务3---<NSThread: 0x608000074340>{number = 3, name = (null)}
由输出的日志我们可以看出,为了方便对比,我们为每个任务设置了休眠时间,当串行队列异步提交的时候,会创建一个新的线程,任务1在等待3秒之后执行,任务2需要等待任务执行完毕2秒之后才能执行,任务3需要等待任务2执行完毕1秒之后才能执行。我们可以发现串行执行所提交的任务,这任务在新的线程中还是一个一个执行的,
3、并行队列异步执行
dispatch_queue_t queue = dispatch_queue_create("ljt", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步提交任务1---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(2);
NSLog(@"异步提交任务2---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"异步提交任务3---%@",[NSThread currentThread]);
});
NSLog(@"队列执行完毕");
});
执行结果:
2017-05-02 13:25:03.881 TestGCD[1556:60317] <NSThread: 0x600000075a40>{number = 1, name = main}
2017-05-02 13:25:03.881 TestGCD[1556:60317] 队列执行完毕
2017-05-02 13:25:04.947 TestGCD[1556:60373] 异步提交任务3---<NSThread: 0x600000079740>{number = 3, name = (null)}
2017-05-02 13:25:05.905 TestGCD[1556:60371] 异步提交任务2---<NSThread: 0x60800007cac0>{number = 4, name = (null)}
2017-05-02 13:25:06.911 TestGCD[1556:60370] 异步提交任务1---<NSThread: 0x600000076380>{number = 5, name = (null)}
这个情况和第二种情况相比,只是队列发生了变化,第二种情况下队列是串行队列,而现在是并发队列,由日志输出我们可以发现,并发队列中的任务是并发执行的,没有固定的顺序。
所以我们可以总结如下:
同步or异步 | 并行队列 | 手动创建的串行队列 | 主队列 |
---|---|---|---|
同步 | 没有开启新的线程</br>串行执行任务 | 没有开启新的线程</br>串行执行任务 | 没有开启新的线程</br>串行执行任务</br>(注意死锁) |
异步 | 有开启新的线程</br>并行执行任务 | 有开启新的线程</br>串行执行任务 | 没有开启新的线程</br>串行执行任务 |
对一个并行队列做同步操作就如同对一个串行队列做异步操作
区别:对一个并行队列做同步操作,队列不会开启新的线程,而对一个串行队列做异步操作会开启新的线程。
<strong>对一个并行队列做同步操作:</strong>
dispatch_queue_t queue = dispatch_queue_create("ljt", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"队列线程---%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
sleep(3);
NSLog(@"同步提交任务1---%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
sleep(2);
NSLog(@"同步提交任务2---%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
sleep(1);
NSLog(@"同步提交任务3---%@",[NSThread currentThread]);
});
NSLog(@"队列执行完毕");
});
输出:
2017-05-02 13:52:07.246 TestGCD[1821:71817] <NSThread: 0x608000076d80>{number = 1, name = main}
2017-05-02 13:52:10.311 TestGCD[1821:71817] 同步提交任务1---<NSThread: 0x608000076d80>{number = 1, name = main}
2017-05-02 13:52:12.334 TestGCD[1821:71817] 同步提交任务2---<NSThread: 0x608000076d80>{number = 1, name = main}
2017-05-02 13:52:13.334 TestGCD[1821:71817] 同步提交任务3---<NSThread: 0x608000076d80>{number = 1, name = main}
2017-05-02 13:52:13.335 TestGCD[1821:71817] 队列执行完毕
任务1,2,3在同一个线程上执行,和最初的队列线程一致
<strong>对一个串行队列做异步操作:</strong>
dispatch_queue_t queue = dispatch_queue_create("ljt", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步提交任务1---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(2);
NSLog(@"异步提交任务2---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"异步提交任务3---%@",[NSThread currentThread]);
});
NSLog(@"队列执行完毕");
});
输出:
2017-05-02 13:53:37.441 TestGCD[1842:72770] <NSThread: 0x60800007c080>{number = 1, name = main}
2017-05-02 13:53:37.442 TestGCD[1842:72770] 队列执行完毕
2017-05-02 13:53:40.502 TestGCD[1842:72805] 异步提交任务1---<NSThread: 0x60800007f200>{number = 3, name = (null)}
2017-05-02 13:53:42.560 TestGCD[1842:72805] 异步提交任务2---<NSThread: 0x60800007f200>{number = 3, name = (null)}
2017-05-02 13:53:43.621 TestGCD[1842:72805] 异步提交任务3---<NSThread: 0x60800007f200>{number = 3, name = (null)}
由日志我们可以看到:
任务1,2,3在同一个线程中执行,和最初的队列线程已经不一致了
串行队列不一定会在同一个线程中执行
我们知道线程是有生命周期的,当我们创建一个线程的时候,这个线程的RunLoop默认是没有开启的,所以当我们提交的一个任务执行完毕之后,这个线程的使命的就完成,就会被系统销毁,当我们在此提交的时候,队列会在线程池中再次拿出一个线程来执行我们所提交的任务。这种情况在什么情况下出现呢?
我们考虑这样一种情况,首先我们创建一个串行队列,向队列中提交一个任务,然后我们30秒之后再向队列中追加一个任务,这个时候新追加的任务的执行线程和最初的的任务线程很有可能不是同一个。
我们来看一下代码:
dispatch_queue_t queue = dispatch_queue_create("ljt", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"异步提交任务---%@",[NSThread currentThread]);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), queue, ^{
NSLog(@"延迟30秒异步提交任务---%@",[NSThread currentThread]);
});
NSLog(@"队列执行完毕");
});
输出:
2017-05-02 14:02:18.584 TestGCD[1870:75267] <NSThread: 0x60800007cc40>{number = 1, name = main}
2017-05-02 14:02:18.585 TestGCD[1870:75267] 队列执行完毕
2017-05-02 14:02:18.585 TestGCD[1870:75300] 异步提交任务---<NSThread: 0x608000269100>{number = 3, name = (null)}
2017-05-02 14:02:48.585 TestGCD[1870:75302] 延迟30秒异步提交任务---<NSThread: 0x60800007cec0>{number = 4, name = (null)}
我们可以看到:
延迟30秒追加的任务和第一次提交的任务所执行的线程不是同一个线程。