面经多线程

每日一问28——GCD(判断队列正确姿势与原因)

2018-01-18  本文已影响27人  巫师学徒

1.疑问

前几天看到有人在问,如何判断当前执行的队列是不是指定队列的问题。网上的资料有很多,但看完以后我反而整不明白了。判断方法有3种:

其中,dispatch_get_current_queue在iOS6之后是被弃用的,苹果只推荐在打印中使用,原因是它容易导致死锁。百度上大部分资料也没有说清楚为什么会导致死锁。(什么叫死锁本篇文章不在详细解释,简单描述就是不同操作相互等待导致互相无法进行)那么问题来了,看下面2个例子:

例子1: dispatch_get_current_queue在一般情况下是否发生死锁
dispatch_queue_t queueA = dispatch_queue_create("com.lyk.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.lyk.queueB", NULL);

dispatch_sync(queueA /*(或者queueB)*/, ^{
        /*function*/
        dispatch_block_t block = ^{
            NSLog(@"NO deadlock!");
        };
        
        if(dispatch_get_current_queue() == queueA) {
            block();
        }
        else {
            dispatch_sync(queueA, block);
        } 

    });

将dispatch_sync闭包中执行的内容看作一个函数,那么有2种执行情况:
1.dispatch_sync(queueA ): dispatch_get_current_queue的返回值就是queueA,所以直接执行block中的代码块,并不会发生死锁。
2.dispatch_sync(queueB ): dispatch_get_current_queue的返回值是queueB,此时会执行else中的代码,同步到queueA任务队列中,执行block代码。这种情况两个队列之间各自执行自己的任务,所以也不会发生死锁。

例子2:dispatch_queue_set_specific & dispatch_get_specific处理一般情况下的队列判断
dispatch_queue_t queueA = dispatch_queue_create("com.lyk.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.lyk.queueB", NULL);
    
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CFSTR("queueA");
    dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
    

    dispatch_sync(queueA, ^{
        dispatch_block_t block = ^{
            NSLog(@"NO deadlock!");
        };
        
        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);

        if (retrievedValue) {
            block();
        } else {
            dispatch_sync(queueA, block);
        }
    });

先简单介绍一下dispatch_queue_set_specific & dispatch_get_specific,这个函数通过给队列设置标记,并且通过指定标记来获取当前当前队列是不是指定的队列。如果当前队列是,则会返回标记字符串"queueA",如果不是则返回nil。

同样,也有2种情况:
1.dispatch_sync(queueA ): dispatch_get_specific的返回值是"queueA",所以直接执行block中的代码块,并不会发生死锁。
2.dispatch_sync(queueB ): dispatch_get_specific的返回值是nil,此时会执行else中的代码,同步到queueA任务队列中,同上,也不会发生死锁。

看完这2个例子,我发现2种方法都可以正确检验队列,并没有发生传说中的死锁现象。那么为什么苹果要废弃dispatch_get_current_queue方法呢?

2.原因

When dispatch_get_current_queue() is called on the main thread, it may
or may not return the same value as dispatch_get_main_queue().

从苹果API上对dispatch_get_current_queue中的描述可以看到,这个函数的返回值不一定是用户想要的那个返回值。但具体为什么,文档上也没有说清楚。
于是我们需要对GCD队列进行进一步的研究,在为什么dispatch_get_current_queue被废弃中找到了可以解释这一切的答案。
原来,队列之间也有指向关系,如图:

955431-a3bcc180f2243ce0.png

可以看出,无论是串行还是并发队列,只要有targetq,都会一层一层地往上扔,直到线程池。所以无法单用某个队列对象来描述“当前队列”这一概念的

而队列的指向关系很可能被修改,而导致dispatch_get_current_queue获取的队列和我们想要的不一致。当设置了B的target queue为A,那么代码中A B都可以看成是当前队列。

dispatch_set_target_queue(queueB, queueA);
dispatch_sync(queueB, ^{
    dispatch_sync(queueA, ^{ /* deadlock! */ });
});

而使用dispatch_get_current_queue获取queueB就还是B,这种情况下就会出现死锁。

dispatch_queue_t queueA = dispatch_queue_create("com.lyk.queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.lyk.queueB", NULL);
    //注意此处,将B的target queue设置为A
    dispatch_set_target_queue(queueB, queueA);

    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{
            NSLog(@"NO deadlock!");
        };
        
        if(dispatch_get_current_queue() == queueA) {
            block();
        }
        else {
            dispatch_sync(queueA, block);
        }
    });

此时,dispatch_get_current_queue()的返回值是queueB,进入else中,同步像queueA添加任务,但这时候的dispatch_sync(queueB, ^{})相当于已经在queueA中添加任务,就导致了同步死锁。

3.解决

从上面我们知道了使用dispatch_get_current_queue()在判断队列时候,可能因为队列层级而导致同步死锁。那么正确的判断方式则是使用dispatch_queue_set_specific & dispatch_get_specific,我们再来看一下使用它设置了目标队列情况下的处理。

    dispatch_queue_t queueA = dispatch_queue_create("com.lyk.queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.lyk.queueB", NULL);
    dispatch_set_target_queue(queueB, queueA);
    
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CFSTR("queueA");
    dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
   
    dispatch_sync(queueA(或者queueB), ^{
        dispatch_block_t block = ^{
            NSLog(@"NO deadlock!");
        };

        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);

        if (retrievedValue) {
            block();
        } else {
            dispatch_sync(queueA, block);
        }
        
    });

情况依然是2种:

4.总结

1.dispatch_get_current_queue()可能会因为队列层级关系导致死锁。不是用来判断指定队列的正确方式
2.dispatch_queue_set_specific & dispatch_get_specific不会因为队列层级关系的变化而找到错误的队列。
3.dispatch_queue_get_label虽然在SDWebImage缓存模块中使用来判断队列是不是ioQueue,但当队层级关系被改变时也会发生与dispatch_get_current_queue()相同的问题。当然sd中没有指定特殊队列关系的情况下不会出现问题。大家可以试一下把下面代码粘到刚刚的例子里去试一下。

const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }

综上所述,判断指定队列正确的姿势应该是使用dispatch_queue_set_specific & dispatch_get_specific

相关文章:

为什么dispatch_get_current_queue被废弃

上一篇下一篇

猜你喜欢

热点阅读