dispatch_queue_set_specific给队列设置特有数据

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dispatch_queue_set_specific给队列设置特有数据相关的知识,希望对你有一定的参考价值。

想要让某个任务在指定队列中以同步的方式执行完后, 继续执行其他任务.

这样说有点抽象, 举个具体的例子, 在队列A中执行任务1, 任务1完成后到串行队列B中执行任务2, 任务2完成后再回到队列A执行后续的任务3,4,...

看起来很简单, 很快写下了这样的代码

dispatch_queue_t queueA = dispatch_queue_create("AA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("BB", NULL);
dispatch_sync(queueA, ^
    // 任务1
    NSLog(@"任务1");
    dispatch_sync(queueB, ^
        // 任务2
        NSLog(@"任务2");
    );
    // 任务3必须在任务2完成后才可以继续
    NSLog(@"任务3");
);

测试完美. 

随着需求的迭代, 产生了新的场景, 此场景需要在queueB下安排queueA下做些事情, 然后就发生了死锁. 代码是这样的

dispatch_queue_t queueA = dispatch_queue_create("AA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("BB", NULL);
self.queueA = queueA;
self.queueB = queueB;

dispatch_async(queueB, ^
    // 复杂的方法调用,做了很多事情

    [self taskAction];

    // 做了很多事情...
);

- (void)taskAction 
    dispatch_sync(self.queueA, ^
        // 任务1
        NSLog(@"任务1");
        dispatch_sync(self.queueB, ^
            // 任务2
            NSLog(@"任务2");
        );
        // 任务3必须在任务2完成后才可以继续
        NSLog(@"任务3");
    );

结果就是必定会出现死锁, 出现queueB -> queueA -> queueB, 那就判断下当前队列是不是queueB, 如果是queueB, 直接执行任务;不是的话, 回到queueB中执行任务

一顿操作后, 发现在gcd中, 系统提供出了一个方法, dispatch_get_current_queue(), 可以获取当前的队列, 但是这个方法又被标记为不推荐使用, 先不管, 试试再说

使用dispatch_get_current_queue() == queueB 判断是不是在queueB中, 看起来很正常, 运行看看,

- (void)taskAction 
    dispatch_sync(self.queueA, ^
        // 任务1
        NSLog(@"任务1");

        dispatch_block_t task2 = ^
            NSLog(@"任务2");
            NSLog(@"正常通过 %@",dispatch_get_current_queue());
        ;
        // dispatch_get_current_queue() 获取到的是queueA
        if (dispatch_get_current_queue() == self.queueB)
            task2();
         else 
            dispatch_sync(self.queueB, task2); // 实际运行,进入此case
        

        // 任务3必须在任务2完成后才可以继续
        NSLog(@"任务3");
    );



// 调用路径不变, 放到下面
dispatch_queue_t queueA = dispatch_queue_create("AA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("BB", NULL);
self.queueA = queueA;
self.queueB = queueB;

dispatch_async(queueB, ^
    // 复杂的方法调用,做了很多事情

    [self taskAction];

    // 做了很多事情...
);

结果还是必定会出现死锁, 在queueA使用dispatch_get_current_queue()获取到的是queueA, 但是实际上这个任务还是在queueB的block中执行的, queueB是一个串行队列, 需要等待前一个任务完成才能执行后面的Block, Block又要求同步执行新的任务, 所以发生了死锁.

上面的现象和在下面是同样的原因, 只不过多了一次queueA的嵌套, 在串行队列中,使用同步方法添加新的任务, 旧的任务永远执行不完, 新的任务永远无法开始.

- (void)viewDidLoad 
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^
        NSLog(@"主线程必定死锁");
    );


// 多了一层全局队列的干扰而已, 实际还是在主队列中
- (void)viewDidLoad 
    [super viewDidLoad];
    dispatch_sync(dispatch_get_global_queue(0, 0), ^
        dispatch_sync(dispatch_get_main_queue(), ^
            NSLog(@"必定死锁");
        );
    );

一个队列不一定对应一个线程,  我们在代码中可以创建成千上万个队列, 但是系统能创建出的线程不是无限的, 系统使用全局并发队列维护了一个线程池, 我们创建的所有的队列最终都会已dispatch_set_target_queue的方式关联到某个全局并发队列中. 也就是说虽然指定了在队列B中的执行任务, 但是队列不代表线程, 队列B可能和队列A使用的是同一个全局队列中的一个串行队列, 此时就会发生串行队列相互等待造成的线程死锁. 

更多目标队列的知识: GCD知识补充.目标队列+GCD循环

下面的2个串行队列, queueA和queueB都关联到了同一个target队列上,

官方推荐使用dispatch_queue_set_specific来获取当前线程的信息,  先看看这个例子

// .m中的全局变量
static int const kQueueAKey = 0;


dispatch_queue_t queueA = dispatch_queue_create("AA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("BB", NULL);

// queueB的目标队列设置为queueA, 那么queueB的任务都将在queueA中执行, 这步很关键
dispatch_set_target_queue(queueB, queueA);

static int const kQueueAKey = 0;
dispatch_queue_set_specific(queueA, &kQueueAKey, (void *)&kQueueAKey, NULL);
dispatch_sync(queueB, ^

    // 这个block想要保证在QueueA进行执行
    // 在当前线程中,获取queueA对应的特有数据,
    // 如果能取到并且值一致,直接执行block,
    // 取不到或者值不一致,同步到线程QueueA执行
    dispatch_block_t block= ^
        NSLog(@"正常通过 %@",dispatch_get_current_queue());
    ;
    // 这个判断可以保证保证线程在QueueA中执行
    void *value = dispatch_get_specific(&kQueueAKey);
    if (value == &kQueueAKey)
        block();
     else 
        dispatch_sync(queueA, block);
    
);

dispatch_get_specific 会从当前线程开始找,

  1. 如果当前线程关联了key-value, 会返回当前线程关联的key-value;
  2. 如果当前线程没有关联key-value,会继续找当前线程的target queue 目标队列, 直到找到全局队列
  3. 全局队列会返回NULL

When called from a block executing on a queue, returns the context for the specified key if it has been set on the queue, otherwise returns the result of dispatch_get_specific() executed on the queue's target queue or NULL if the current queue is a global concurrent queue.

资料来源, 官方文档上的注释.

但是我们的需求要求queueA和queueB是各自独立的队列, 根本没有调用过dispatch_set_target_queue, 在queueA中获取queueB的key获取到的是NULL, 所以还是不能满足要求.

最后只能使用不那么优雅的方式解决了. 

- (void)taskAction 
    dispatch_sync(self.queueA, ^
        // 任务1
        NSLog(@"任务1");
        dispatch_async(self.queueB, ^
            NSLog(@"任务2");
            // ....

            dispatch_async(self.queueA, ^
                NSLog(@"任务3");
            );
        );

    );

虽然不那么优雅, 但是还是需求要紧, 而且还了解到了dispatch_get_current_queue的特性, dispatch_get_specific没有想象的那么好用, 也算是补充了自己的盲区.

最后, 在不改变结构的情况下, 有没有更好的方法可以做到不死锁的.

dispatch_queue_t queueA = dispatch_queue_create("AA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("BB", NULL);
self.queueA = queueA;
self.queueB = queueB;
dispatch_queue_set_specific(queueA, &kQueueAKey, (void *)&kQueueAKey, NULL);
dispatch_queue_set_specific(queueB, &kQueueBKey, (void *)&kQueueBKey, NULL);

dispatch_async(queueB, ^
    // 复杂的方法调用,做了很多事情

    [self taskAction];

    // 做了很多事情...
);


- (void)taskAction 
    dispatch_sync(self.queueA, ^
        // 任务1
        NSLog(@"任务1");

        dispatch_block_t block= ^
            NSLog(@"任务2");
        ;
        /*如何判断当前线程是不是queueB*/
//        BOOL result = dispatch_get_current_queue() == self.queueB; // 当前队列为queueA,不可以
        BOOL result = dispatch_get_specific(&kQueueBKey) == &kQueueBKey; // 获取为NULL,不可以
        if (result) 
            block();
         else 
            dispatch_sync(self.queueB, block);
        

        NSLog(@"任务3");
    );

以上是关于dispatch_queue_set_specific给队列设置特有数据的主要内容,如果未能解决你的问题,请参考以下文章

GCD的Queue-Specific