GCD串行同步死锁原因及解决方法

串行同步的使用

GCD中的同步串行常用于线程安全方案。串行队列的一个特点是一个任务执行完成才能执行下一个务,而同步函数dispatch_sync会阻塞当前线程,直到当前任务执行完成才能往下执行代码。
demo示例:

    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.hello-world.djx", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
     dispatch_sync(queue, ^{ 
            NSLog(@"2");
        });
    NSLog(@"1");

打印结果:

1
2
3

死锁原因

但是同步串行有个安全隐患,就是容易死锁。以下是几个常见的死锁模型:

  • 在主线程同步主队列
 dispatch_queue_t mainQueue = dispatch_get_main_queue();
 NSLog(@"1");
  dispatch_sync(mainQueue, ^{
        NSLog(@"2");
    });
    NSLog(@"3");

死锁原因分析:这里面只能打印1,然后就死锁。原因在于当前代码执行的是主线程,dispatch_sync函数阻塞了主线程,由于主队列是串行队列,而且主队列只能在主线程执行,这时候就出现了主队列block等待主线程往下执行,而主线程正在阻塞等待主队列的block执行完才能执行,出现了循环等待,导致死锁。

  • 在当前队列的block中同步当前队列
 // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.hello-world.dongjianxiong", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步
        dispatch_sync(queue, ^{ //queue是个串行队列,block要执行完才能执行3block,
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");

demo只打印了1,5,2然后就然后死锁。
死锁原因分析:首先queue是个串行队列,代码执行到2的时候,当前的线程正在执行队列的block2,而此时dispatch_sync函数阻塞了线程同步执行block3,由于串行队列的特点,block3必须等到block2执行完成才能执行,而此时正在执行block2的线程被阻塞,必须的dispatch_sync函数执行完才能继续往下执行,这样就形成了相互等待的局面,导致死锁。

其实以上两个模型原理上都是一样的,都是在当前执行该串行队列的线程中同步执行该队列,导致相互等待,导致死锁。

解决办法

改用异步函数调用

比如上面的主队列同步的问题,改成下面的方式:

 dispatch_queue_t mainQueue = dispatch_get_main_queue();
 NSLog(@"1");
  dispatch_async(mainQueue, ^{
        NSLog(@"2");
    });
    NSLog(@"3");

这时候异步函数dispatch_async不会阻塞当前线程,它会直接执行3然后在执行2。

改用并行队列

比如上面的第二个demo:

 // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.hello-world.dongjianxiong", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步
        dispatch_sync(queue, ^{ 
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");

由于是并行队列,虽然block2被阻塞了,但是block3并不需要等到block2执行完它才能执行,block3会继续执行完成,然后block2继续执行4任务。不会死锁。

判断当前线程是否正在执行当前队列的任务

对于以上两种方法都有一定的问题,使用异步的话这代码执行逻辑会发生更改,有可能不符合我们的预期;而对于第二种方法,虽然同步能保证在当前线程执行,并行队列不能像串行队列那样一个执行完在执行一个,在多线程环境就不能保证任务按顺序完成,也不符合我们的需求。所以这时候我们就要判断当前线程是否正在执行当前队列的任务,如果是,说明当前任务已经在队列里面,可以直接执行;如果不是,我们就往队列里添加,交由队列去执行。

  • 主线程
    通过[NSThread isMainThread]判断是不是在主线程:
 if ([NSThread isMainThread])// 如果是主线程
        {
            //直接执行
            NSLog(@"4");

        } else {//如果不是主线程,
            
            NSLog(@"5");
            // 如果我们希望同步执行,则
            dispatch_sync(dispatch_get_main_queue(), ^{
                //同步到主队列执行
                NSLog(@"6");
            });
            
            //如果我们希望异步执行,则
            dispatch_async(dispatch_get_main_queue(), ^{
                //交由主队列异步到主线程执行
                NSLog(@"7");
            });
        }

备注:网上有些通过以下方法来判断是否是主线程是不太严谨的:

if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0)
{
// 如果是主线程直接执行
  } else {
// 如果是其他线程,如果我们希望同步执行,则
  dispatch_sync(dispatch_get_main_queue(), ^{
        //同步到主队列执行
    });
}

这里的问题是,这个只能判断是不是主队列,不能判断是否是主线程。原因是主线程不只是执行主队列,主线程还可以执行其他队列,比如下面的代码:

    dispatch_queue_t squeue = dispatch_queue_create("hahah", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(squeue, ^{
        const char *c1 = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
        const char *c2 = dispatch_queue_get_label(dispatch_get_main_queue());
        if (strcmp(c1, c2) == 0)
        {
          } else {
        }
    });
}

如果在主线程执行的话,由于dispatch_sync同步函数的原因,队列squeue的block会在主线程执行,这时候通过这个方法dispatch_queue_get_label获取的队列label是squeue的,不是主队列的,显然这样自然无法判断出是否是主线程的。

  • 普通串行队列
    我们自己创建一个串行队列,利用dispatch_queue_set_specific标记该队列,通过dispatch_queue_get_specific来获取该队列,让任务添加到我们指定的队列执行:
static void *kQueuekey = &kQueuekey;
- (void)textSycDemo3{
    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.hello-world.dongjianxiong", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_set_specific(queue, kQueuekey, kQueuekey, NULL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        NSLog(@"%@",[NSThread currentThread]);
        // 判断是否是正在执行当前队列
        if (dispatch_get_specific(kQueuekey) == kQueuekey) {
            //如果是,直接执行
            NSLog(@"3");
        }else{
            //如果不是,同步到当前队列执行
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
        }
         NSLog(@"4");
    });
    NSLog(@"5");
}

dispatch_queue_get_specific通过判断当前队列是否我们的目标队列,如果是就直接执行,如果不是就添加到我们的目标队列执行。这样就避免了在同一个队列任务里进行同步执行该队列导致死锁的问题。

你可能感兴趣的:(GCD串行同步死锁原因及解决方法)