XiaoboTalk

GCD 在 iOS 中的使用技巧

看过了很多多线程的文章,也写过很多多线程相关的博客;也知道了多线程在操作系统层面,是由中断、原语、信号量、互斥等底层原理实现;但真正遇到多线程问题时,还是不能很快的定位问题出在了哪里。

iOS GCD 的实践


在处理复杂且庞大的多线程项目时,我并不喜欢用 GCD,而更倾向于 NSOperation,原因有二:
1、GCD 的语法格式 (block),会导致项目中 GCD 的代码块到处乱飘,不利于代码结构化管理,以及将多线程能力收缩到一处。
2、GCD 设置中很难优雅的设置操作依赖。
由于 GCD 直接将线程的概念隐藏,暴露给使用者只有两个概念,同步(异步)队列

同步


一句话理解同步,就是要不要等待。同步即等待返回,异步就是无需等待,继续往后执行。
NSLog(@"1、beigin sync, waiting!"); dispatch_queue_t queue1 = dispatch_queue_create("Queue1", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue1, ^{ NSLog(@"2、sync excute"); }); NSLog(@"3、end sync, finish!");
上面代码的执行顺序是 1-2-3;即使将队列改为并发的全局队列也一样。并且一般情况下,同步执行的代码块,不会开新的线程,即当前在什么线程,同步执行的代码块就在什么线程执行。上述代码 1-2-3 都在同一个线程执行
为什么不开线程?很多人都会误以为,用了队列 queue1,就一定开了线程,但并不是。队列并不能左右是否线程,队列只能规定任务的调度顺序。但也有例外,就是主队列 (main queue),这个后文在说。
队列是一种排列任务的方式,串行队列要求:队列中的任务必须一个接一个的执行。队列中任务的顺序,根据添加的先后时机而定。而并发队列中,任务都是并发执行的,并且系统会根据目前线程池的情况,决定开多少线程执行这些并发任务。并发和并行的概念很容易混淆,但其实二者有本质区别:
并行:多个任务真正的同时执行,英文叫 Parallel,该次本意是平行,通过平行一词就能很好的理解并行,多个任务是平行关系。加入一个处理器有四个核心,那么并行就是指,4个任务分别被4个核心同时处理。 并发:则指的是,多个任务具备同时执行的能力,任务彼此之间不会互相等待。并发的任务不一定是并行执行的,大多数情况下,并发的任务可能都由一个处理器核心执行,处理器快速的在多个任务之间切换执行。例如每个任务被分配了多个处理器的时间片,处理器就会执行一个时间片 A 任务,然后再执行一个时间片的 B 任务,然后又执行一个时间片的 A 任务。直到 A B 任务都执行完。可以看到任务的执行不是原子性的,而是被分配到若干个时间片上。

队列和同步的冲突 引发死锁


下面的代码在主线程执行会引发一个死锁:
NSLog(@"running on main thread"); // Code 1 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"deadlock"); // Code 2 });
最近在解决多线程时,我才真正理解这里为什么死锁。根本原因是同步机制和串行队列的任务调度之间的矛盾,引发了死锁。首先,主队列是一个串行队列,这就要求,主队列中的任务必须一个接一个的执行,Code 1所在的执行任务已经在主队列中了,不妨称为 A 任务,并且正在执行。此时又向该串行队列添加了 B 任务,即Code 2代码。因为 A 任务正在执行,所以 B 任务不能被处理器调度。但这里用了sync同步执行,即表示当前执行的 A 任务需要等待 B 任务执行完,才能继续。但串行队列又要求 B 任务只有等 A 任务执行完,才能被处理器调度。所以出现了互相等待的死锁现象。
并不是只有主队列会死锁,只要是在一个串行队列中,继续同步添加任务,都会死锁。
dispatch_queue_t queue = dispatch_queue_create("Test_queue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ NSLog(@"1、start sync on queue"); dispatch_sync(queue, ^{ // deadlock NSLog(@"2.1、sync on queue task"); }); });
上边的代码同样会死锁,先用 async 向 queue 中添加了一个任务,此时会开一个线程,然后继续用同步的方式向 queue 中添加任务,就会触发同步机制和串行队列任务调度机制的冲突,导致死锁,原理和主队列一样。

主队列的特殊性


前边提到,队列本身只是安排任务的调度方式,本身不具备开线程能力。而加上异步后,才会开线程,为了加深理解,可以多看看下边的代码示例:
dispatch_queue_t queue1 = dispatch_queue_create("Test_queue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue2 = dispatch_queue_create("Inner_queue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue1, ^{ NSLog(@"2、start async on queue1, thread: %@", [NSThread currentThread]); dispatch_sync(queue2, ^{ NSLog(@"2.1、sync on queue2 task 1111111, thread: %@", [NSThread currentThread]); }); dispatch_sync(queue2, ^{ NSLog(@"2.2、sync on queue2 task 2222222, thread: %@", [NSThread currentThread]); }); }); dispatch_sync(queue2, ^{ NSLog(@"1、sync on queue2 the end !!!!!!, thread: %@", [NSThread currentThread]); });
上述代码的执行顺序是:1 - 2 - 2.1 - 2.2,这个比较好理解。同时,2 - 2.1 - 2.2 三条打印,都在同一个线程,原因这里不再赘述。但是主队列比较特殊,系统让主队列中的任务都在主线程执行:
dispatch_queue_t queue1 = dispatch_queue_create("Test_queue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue1, ^{ NSLog(@"1、start a new thread"); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"3、async on main thread"); }); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2、sync on main thread"); }); });
上述代码的执行顺序: 1-2-3,并且 2 、3 代码都会在主线程执行。

利用同步与队列实现任务的依赖执行


有时候,任务的依赖执行很有用,比如延长 app 在进入后台的执行能力:
- (void)excuteWhenAppEnterBackground { NSLog(@"0、running on main thread"); dispatch_queue_t queue1 = dispatch_queue_create("Test_queue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue2 = dispatch_queue_create("Inner_queue", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue1, ^{ dispatch_async(queue2, ^{ NSLog(@"1、Perform task 1 on queue2 in background"); }); dispatch_async(queue2, ^{ NSLog(@"2、Perform task 2 on queue2 in background"); }); }); dispatch_sync(queue2, ^{ NSLog(@"3、This will perform on main thread in queue2"); }); }
上述代码执行顺序:0-1-2-3,同时 1、2 在同一个子线程执行,0、3 均在主线程执行。同时由于 1-2-3 任务都在 queue2 这个串行队列中,且 3 号任务最晚加入到队列,所以 3 必须等待 1-2 执行完才能执行。如果 1-2 执行的是耗时任务,例如后台下载,那么 3 号打印就需要再主线程等待一会才能执行。由于会卡主线程,所以这样的代码一般都用在 app 进入后台后执行,从而延长 app 的后台执行时间。