如何安全使用dispatch_sync

概述

iOS开发者在与线程打交道的方式中,使用最多的应该就是GCD框架了,没有之一。GCD将繁琐的线程抽象为了一个个队列,让开发者极易理解和使用。但其实队列的底层,依然是利用线程实现的,一样会有死锁的问题。本文将探讨如何规避disptach_sync接口引入的死锁问题。安全


GCD基础

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

async VS.sync

disptach_async是异步扔一个blockqueue中,即扔完我就无论了,继续执行个人下一行代码。实际上当下一行代码执行时,这个block还未执行,只是入了队列queuequeue会排队来执行这个block测试

disptach_sync则是同步扔一个blockqueue中,即扔了我就等着,等到queue排队把这个block执行完了以后,才继续执行下一行代码。ui


为何要使用sync

disptach_sync主要用于代码上下文对时序有强要求的场景。简单点说,就是下一行代码的执行,依赖于上一行代码的结果。例如说,咱们须要在子线程中读取一个image对象,使用接口[UIImage imageNamed:],但imageNamed:实际上在iOS9之后才是线程安全的,iOS9以前都须要在主线程获取。因此,咱们须要从子线程切换到主线程获取image,而后再切回子线程拿到这个imagespa

// ...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执行完才能执行。这时死锁就产生了,block0block1互相等待执行,当前线程就卡死在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正在主线程中执行,而且同步等待子线程执行完block1block1又同步等待主线程执行完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...


如何定位死锁问题

1.死锁监测和堆栈上报机制

要定位死锁的问题,咱们须要知道在哪一行代码上死锁了,以及为何会出现死锁。一般只要知道哪一行代码死锁了,咱们就能经过代码分析出问题所在了。因此,若是死锁的时候,咱们可以把堆栈上报上来,就能知道哪一行代码死锁了。这里须要有完善的死锁监测和堆栈上报机制

2.打印日志

若是暂时没有人力或者技术支撑你去搭建完善的死锁监测和堆栈上报机制,那么你能够作一件简单的事情以协助你定位问题,那就是打印日志。在dispatch_sync或者加锁以前,打印一条日志。这样在用户反馈问题,或者测试重现问题的时候,提取日志即可分析出卡死的代码处。


如何安全使用dispatch_sync

答案是,尽可能不要使用。没有哪个接口是能够保证绝对安全的。必需要使用dispatch_sync的时候,尽可能使用dispatch_sync_on_main_queue这个API。

做者:Joey_Xu 连接:https://www.jianshu.com/p/b3227582037d 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
相关文章
相关标签/搜索