65

SDWebImage学习笔记之dispatch_sync

 5 years ago
source link: http://www.cocoachina.com/ios/20180712/24125.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

之前学习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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK