Contents
  1. 1. 信号量
  2. 2. 死锁
  3. 3. 更变态的死锁

信号量

常见用信号量把异步操作转换成同步操作,先看常见异步操作

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    sleep(5);
    NSLog(@"耗时任务执行完毕");
});

NSLog(@"我先执行");

执行的结果是

2019-07-14 18:17:18.541811+0800 DeadLock[28921:1279001] 我先执行
2019-07-14 18:17:23.546835+0800 DeadLock[28921:1279053] 耗时任务执行完毕

如果想同步的等待 block 里的操作再顺序执行下面的代码可以把 dispatch_async 改成 dispatch_sync。这样代码所在的当前线程就会等待 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) 队列里的该 block 任务被某个后台线程取走执行完毕再进行下一步。代码如下

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    sleep(5);
    NSLog(@"耗时任务执行完毕");
});

NSLog(@"我先执行");

这段代码的执行结果是

2019-07-14 18:20:14.998642+0800 DeadLock[28953:1282252] 耗时任务执行完毕
2019-07-14 18:20:14.998776+0800 DeadLock[28953:1282252] 我先执行

有时候我们并不能这样做,比如把别封装好的异步操作拿来用时就不行。dispatch_async 是别人写在内部的,无法改变。暴露出来的接口只有入口、出口。但我们可以使用信号量来完成这个任务。代码如下

dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    sleep(5);
    NSLog(@"耗时任务执行完毕");
    dispatch_semaphore_signal(_sema);
});
dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);

NSLog(@"同步等待,等到了,该我执行了");

执行结果是

2019-07-14 18:24:16.826015+0800 DeadLock[29161:1287841] 耗时任务执行完毕
2019-07-14 18:24:16.826136+0800 DeadLock[29161:1287793] 同步等待,等到了,该我执行了

可以看到我们成功地让 block 里面的代码先执行,下一步动作等它执行完毕再进行动作。

但是如果对方提供的 api 的出口他先用 dispatch_async 到主线程包一下再给出来,那么这段代码就要死锁了。发生死锁的代码如下

dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    sleep(5);
    NSLog(@"耗时任务执行完毕");
    dispatch_async(dispatch_get_main_queue(), ^{
        /// 这里是 api 暴露出来的出口位置
        dispatch_semaphore_signal(_sema);
    });
});
dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);

NSLog(@"同步等待,等到了,该我执行了");

block 外的代码永远执行不了,主线程堆栈如下

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  * frame #0: 0x00007fff5cb5e266 libsystem_kernel.dylib`semaphore_wait_trap + 10
    frame #1: 0x00000001003435cd libdispatch.dylib`_dispatch_sema4_wait + 16
    frame #2: 0x0000000100343d94 libdispatch.dylib`_dispatch_semaphore_wait_slow + 98

可以看到主线程在一直等待 dispatch_semaphore_wait 执行完毕,而该信号量迟迟得不到更新。那为什么信号量没得到更新呢?

死锁

分析一下代码的执行顺序:

  1. 整个代码块运行的环境是主线程,它肯定是它上面的代码以及它下面的代码打包在一起作为整体任务(M1)提交到了主队列。主线程的 runloop 不断循环取队列里的任务出来执行,取到该任务执行到了第一个 dispatch_async 时,它把 block 里的任务打包放到了优先级是 DISPATCH_QUEUE_PRIORITY_HIGH 的队列里(几乎与此同时,某个处于 DISPATCH_QUEUE_PRIORITY_HIGH 优先级的后台线程开始执行该 block 块)。然后执行 wait 函数,停在这里。主线程的状态为卡住不动。
  2. block 里的耗时操作执行完毕之后紧接着又执行了第二个 dispatch_async 操作,操作的内容是向主队列提交一个任务(M2),任务的内容是更新信号量。提交完毕,该线程的该任务也执行完毕,接着去做其他 DISPATCH_QUEUE_PRIORITY_HIGH 优先级的任务

现在我们看下主队列的情况,在步骤 1 中主线程取了一个任务执行,但是被信号量卡住了,该任务还未完毕。主队列为
[M1, …, M2],现在指针指向 M1:正在执行 M1 的内容。而 M1 想要执行完毕返回,依赖于 M2。因此死锁。

不光是主队列、主线程,其他队列、线程一样会出现死锁的问题。这里新创建的 com.deadlock 队列跟上面的主队列情况一样。

dispatch_queue_t queue = dispatch_queue_create("com.deadlock", DISPATCH_QUEUE_SERIAL);
//// -------  1  ---------
dispatch_async(queue, ^{
    // 现在在队列 “com.deadlock”,是 M1
    
    dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        sleep(5);
        NSLog(@"耗时任务执行完毕");
        //// -------  2  ---------
        dispatch_async(queue, ^{ /// 这里跟外部的队列一样,往同一个队列提交任务
            // 现在在队列 “com.deadlock”,是 M2
            /// 这里是 api 暴露出来的出口位置
            dispatch_semaphore_signal(_sema);
        });
    });
    dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);
    
    NSLog(@"同步等待,等到了,该我执行了");
});

看一下 com.deadlock 对应线程的堆栈,还是在等待信号量。

* thread #5, queue = 'com.deadlock'
  * frame #0: 0x00007fff5cb5e266 libsystem_kernel.dylib`semaphore_wait_trap + 10
    frame #1: 0x00000001003435cd libdispatch.dylib`_dispatch_sema4_wait + 16
    frame #2: 0x0000000100343d94 libdispatch.dylib`_dispatch_semaphore_wait_slow + 98

结论:只要 1 跟 2 位置派发到的队列是同一个就会死锁

但是,如果 queue 队列是并发队列情况又不一样了。我们把队列类型改成并发队列,同时加上一些日志信息帮助分析

dispatch_queue_t queue = dispatch_queue_create("com.deadlock", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // 现在在队列 “com.deadlock”,是 M1
    NSLog(@"M1 任务开始 current thread %@", [NSThread currentThread]);
    dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        sleep(5);
        NSLog(@"耗时任务执行完毕");
        dispatch_async(queue, ^{ /// 这里跟外部的队列一样,往同一个队列提交任务
            // 现在在队列 “com.deadlock”,是 M2
            /// 这里是 api 暴露出来的出口位置
            NSLog(@"M2 任务开始 current thread %@", [NSThread currentThread]);
            dispatch_semaphore_signal(_sema);
        });
    });
    dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);
    NSLog(@"M1 任务继续执行 current thread %@", [NSThread currentThread]);
});

打印出来日志:

M1 任务开始 current thread <NSThread: 0x600001761240>{number = 2, name = (null)}
耗时任务执行完毕
M2 任务开始 current thread <NSThread: 0x600001779840>{number = 3, name = (null)}
M1 任务继续执行 current thread <NSThread: 0x600001761240>{number = 2, name = (null)}

可以看到 M1 任务被线程 2 取走执行,M2 任务被线程 3 取走执行。当 M1 处于上诉的卡住状态时,M2 在线程 3 中顺利执行并更新信号量。内核一路通知到操作系统再到 App 再到线程 2,M1 任务得以继续执行。

我印象中 TMCache 的死锁就好像发生在这种情况下。现在是刚巧还有线程 3 取走了 M2 任务来执行,如果系统资源不够了,分配不了线程来执行 M2 了,就死锁了。几乎所有的线程都在做 M1 任务,等待它的 M2 任务来继续。

更变态的死锁

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//// -------  1  ---------
dispatch_async(queue, ^{
    // 现在在队列 “com.deadlock”,是 M1
    dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        sleep(5);
        NSLog(@"耗时任务执行完毕");
        //// -------  2  ---------
        dispatch_async(queue, ^{ /// 这里跟外部的队列一样,往同一个队列提交任务
            // 现在在队列 “com.deadlock”,是 M2
            /// 这里是 api 暴露出来的出口位置
            NSLog(@"M2 任务开始 current thread %@", [NSThread currentThread]);
            dispatch_semaphore_signal(_sema);
        });
    });
    dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);
});]

queue 的生成方式变成向系统提交一个 DISPATCH_QUEUE_PRIORITY_HIGH 优先级的任务,如果刚好这两次提交任务安排在了一个线程里,就死锁了。

Contents
  1. 1. 信号量
  2. 2. 死锁
  3. 3. 更变态的死锁