iOS开发者在与线程打交道的方式中,使用最多的应该就是GCD框架了,没有之一。GCD将繁琐的线程抽象为了一个个队列,让开发者极易理解和使用。但其实队列的底层,依然是利用线程实现的,一样会有死锁的问题。本文将探讨如何规避disptach_sync
接口引入的死锁问题。安全
GCD最基础的两个接口框架
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
第一个参数queue
为队列对象,第二个参数block
为block对象。这两个接口能够将任务block
扔到队列queue
中去执行。异步
开发者使用最频繁的,就是在子线程环境下,须要作UI更新时,咱们能够将任务扔到主线程去执行,async
dispatch_sync(dispatch_get_main_queue(), block); dispatch_async(dispatch_get_main_queue(), block);
而dispatch_sync(dispatch_get_main_queue(), block)
有可能引入死锁的问题。post
disptach_async
是异步扔一个block
到queue
中,即扔完我就无论了,继续执行个人下一行代码。实际上当下一行代码执行时,这个block
还未执行,只是入了队列queue
,queue
会排队来执行这个block
。测试
而disptach_sync
则是同步扔一个block
到queue
中,即扔了我就等着,等到queue
排队把这个block
执行完了以后,才继续执行下一行代码。ui
disptach_sync
主要用于代码上下文对时序有强要求的场景。简单点说,就是下一行代码的执行,依赖于上一行代码的结果。例如说,咱们须要在子线程中读取一个image
对象,使用接口[UIImage imageNamed:]
,但imageNamed:
实际上在iOS9之后才是线程安全的,iOS9以前都须要在主线程获取。因此,咱们须要从子线程切换到主线程获取image
,而后再切回子线程拿到这个image
,spa
// ...currently in a subthread __block UIImage *image; dispatch_sync_on_main_queue(^{ image = [UIImage imageNamed:@"Resource/img"]; }); attachment.image = image;
这里咱们必须使用sync
。线程
假设当前咱们的代码正在queue0
中执行。而后咱们调用disptach_sync
将一个任务block1
扔到queue0
中执行,日志
// ... currently in queue0 or queue0's corresponding thread. dispatch_sync(queue0, block1);
这时,dispatch_sync
将等待queue0
排队执行完block1
,而后才能继续执行下一行代码。But,当前代码执行的环境也是queue0
。假设当前执行的任务为block0
。也就是说,block0
在执行到一半时,须要等到本身的下一个任务block1
执行完,本身才能继续执行。而block1
排队在后面,须要等block0
执行完才能执行。这时死锁就产生了,block0
和block1
互相等待执行,当前线程就卡死在dispatch_sync
这行代码处。
咱们发现的卡死问题,通常都是主线程死锁。一种较为常见的状况是,自己就已经在主线程了,还同步向主线程扔了一个任务:
// ... currently in the main thread dispatch_sync(dispatch_get_main_queue(), block);
YYKit中提供了一个同步扔任务到主线程的安全方法:
/** Submits a block for execution on a main queue and waits until the block completes. */ static inline void dispatch_sync_on_main_queue(void (^block)()) { if (pthread_main_np()) { block(); } else { dispatch_sync(dispatch_get_main_queue(), block); } }
其方式就是在扔任务给主线程以前,先检查当前线程是否已是主线程,若是是,就不用调用GCD的队列调度接口dispatch_sync
了,直接执行便可;若是不是主线程,那么调用GCD的dispatch_sync
也不会卡死。
但事实上并非这样的,dispatch_sync_on_main_queue
也可能会卡死,这个安全接口并不安全。这个接口只能保证两个block
之间不因互相等待而死锁。多于两个block
的互相依赖就一筹莫展了。
举个例子,假设queue0
是一个子线程的队列:
/* block0 */ // ... currently in the main thread. dispatch_sync(queue0, ^{ /* block1 */ // ... currently in queue0's corresponding subthread. dispatch_sync_on_main_queue(^{ /* block2 */ }); });
在上述代码中,block0
正在主线程中执行,而且同步等待子线程执行完block1
。block1
又同步等待主线程执行完block2
。而当前主线程正在执行block0
,即block2
的执行须要等到block0
执行完。这样就成了block0
-->block1
-->block2
-->block0
...这样一个循环等待,即死锁。因为block1
的环境是子线程,因此安全API的线程判断不起任何做用。
另举一个例子:
/* block0 */ // ... currently in the main thread. [[NSNotificationCenter defaultCenter] postNotificationName:@"aNotification" object:nil]; // ... in another context [[NSNotificationCenter defaultCenter] addObserverForName:@"aNotification" object:nil queue:queue0 usingBlock:^(NSNotification * _Nonnull note) { /* block1 */ // ... currently in queue0's corresponding subthread. dispatch_sync_on_main_queue(^{ /* block2 */ }); }];
因为通知NSNotification
的执行是同步的,这里会出现和上一例同样的死锁状况:block0
-->block1
-->block2
-->block0
...
要定位死锁的问题,咱们须要知道在哪一行代码上死锁了,以及为何会出现死锁。一般只要知道哪一行代码死锁了,咱们就能经过代码分析出问题所在了。因此,若是死锁的时候,咱们可以把堆栈上报上来,就能知道哪一行代码死锁了。这里须要有完善的死锁监测和堆栈上报机制。
若是暂时没有人力或者技术支撑你去搭建完善的死锁监测和堆栈上报机制,那么你能够作一件简单的事情以协助你定位问题,那就是打印日志。在dispatch_sync
或者加锁以前,打印一条日志。这样在用户反馈问题,或者测试重现问题的时候,提取日志即可分析出卡死的代码处。
答案是,尽可能不要使用。没有哪个接口是能够保证绝对安全的。必需要使用dispatch_sync
的时候,尽可能使用dispatch_sync_on_main_queue
这个API。