转载

SDWebImage学习笔记之dispatch_sync

前言

之前学习GCD的时候,在很多文章中看到过这段段代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}

结果只会输出1,并造成主线程死锁。这些文章对死锁的原因也做了解释,且只要把dispatch_sync改为dispatch_async,就可以输出顺序为1->3->2的结果。

当时以为自己学会了,但是等到用的时候还是一头雾水,之前理解的东西就只是些皮毛。趁着这次学习SDWebImage库,重温了这段代码,才明白自己当时的问题所在。

误区

上述这段代码用来表明死锁的方式没有错,但是对我造成的误区是,笔者以为导致死锁的代码是NSLog(@"2")和NSLog(@"3")。dispatch_sync会阻塞主线程,必须等待NSLog(@"2")执行完后NSLog(@"3")代码才可以执行("3"等待"2")。但是NSLog(@"3")先于NSLog(@"2")被插入到主线程队列中去,NSLog(@"2")必须等到NSLog(@"3")执行完才可以执行("2"等待"3"),这种相互等待的过程导致了主线程死锁。

这种理解方式一直让我深信不疑,但是一次偶然的机会,看到了下面这段代码后,才明白原来的理解方式是错误的。(笔者认为,下面这段代码更能够表现死锁问题。)

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
}

这段代码没有NSLog(@"1"),也没有NSLog(@"3"),但是依然会造成死锁,顿时我就疑惑了,为什么没有NSLog(@"3")还会有问题呢?

解惑

在解惑之前,先来看dispatch_sync方法的描述。苹果官方对它的描述是:

Submits a block object for execution on a dispatch queue and waits until that block completes.

翻译过来就是:

提交一个块对象以在分派队列上执行,并等待该块完成。

dispatch_sync方法有两个参数:queue和block。queue表示用于执行block的队列。block是一个代码块,包含了要队列中执行的代码。

队列有三种:

  1. 主队列。主队列运行在主线程中,是一个串行队列。

  2. 串行队列。遵循先进先出(FIFO)的原则,每次只能执行一个操作。

  3. 并发队列。遵循先进先出(FIFO)的原则,每次可以执行多个操作。

队列和执行方式的关系如下表所示:

同步异步
串行队列(主队列)在主线程中执行在主线程中执行
串行队列(非主队列)在当前线程中执行在新建线程中执行
并发队列在当前线程中执行在新建线程中执行

- (void)viewDidLoad {dispatch_sync是同步执行方法,会等待第一个参数queue执行完第二个参数block中的代码后,才可以执行dispatch_sync后面的代码。现在,回头再来看上面的代码。

    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
}

dispatch_sync方法定义在viewDidLoad方法内,viewDidLoad在主线程中执行,所以dispatch_sync方法也在主线程中执行。

dispatch_sync方法的第一个参数传入的时候主队列,说明第二个参数传入的block中的代码也在主线程中运行。

下面这段话很关键!
下面这段话很关键!
下面这段话很关键!

dispatch_sync方法等待queue中的block执行,queue是主队列。但是,由于主队列是串行队列,dispatch_sync比block先加入到主队列中,所以block要等待dispatch_sync执行完才可以执行,这样相互等待的方式最终导致了死锁。

至此,我心中的疑云被解开了,dispatch_sync和block的相互等待才是导致主线程死锁的“真凶”!

解决方法

解决方法有很多,常见的有以下两种:

  1. 将dispatch_sync替换成dispatch_async,即将同步执行改为异步执行。dispatch_async方法会立即返回,允许后面的代码执行,且dispatch_async的block会插入到主队列的末尾,等到后面的代码执行完毕后,再执行block。

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"2");
    // 打印当前队列的标签
    NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
});
// print 2
// print com.apple.main-thread 主队列标签
  1. 创建一个串行队列(非主队列)或并发队列。创建了一个新的队列,不会影响主队列执行,因为是同步执行方法,所以主队列会等到新队列执行完block后才继续执行。

// 串行队列
dispatch_queue_t queue = dispatch_queue_create("queue"nil);
// 并发队列
// dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue,  nil), ^{
    NSLog(@"2");
    // 打印当前队列的标签
    NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
});

// print 2
// print queue 串行队列标签

以上两种解决方法,代码依然运行在主线程,前一种方法改为了异步执行,后一种方法创建新的队列。


SDWebImage中的一段宏定义

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {
        block();
    } else {
        dispatch_async(queue, block);
    }
#endif

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif

dispatch_queue_async_safe方法接收两个参数queue和block,该方法判断传入queue的标签是否等于当前队列的标签,返回YES,则在queue(当前队列)中执行block,否则将block添加到queue中异步执行。

dispatch_main_async_safe方法接收一个参数block,内部调用dispatch_queue_async_safe方法,传入queue的值为主队列dispatch_get_main_queue()和block,dispatch_queue_async_safe方法判断当前队列是否为主队列,返回YES则直接执行block的代码,否则将block添加到主队列中异步执行。

这个宏可以保证,block中的代码一定在主队列中执行。


总结

其实理解dispatch_sync的关键就是搞清楚执行代码所在的队列是哪个,只要执行dispatch_sync的队列跟dispatch_sync第一个参数传入的队列相同,就会产生死锁。

还有,看苹果的官方文档是最正确的方式,虽然英文看上去累一些,但是能够准确表达每一个类,每一行代码的意义。中文翻译过来的语言都会带上作者自己的一些理解。官方文档对于GCD的描述如下:

GCD provides and manages FIFO queues to which your application can submit tasks in the form of block objects. Work submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes.

请认真研读下面这句话,会对理解调度队列和线程之间的关系有帮助。

Work submitted to dispatch queues are executed on a pool of threads fully managed by the system.

GCD官方文档地址:https://developer.apple.com/documentation/dispatch#//apple_ref/doc/uid/TP40008079

正文到此结束
Loading...