对dispatch_queue,reentrancy和deadlocks的澄清

时间:2015-11-05 11:20:31

标签: ios multithreading macos grand-central-dispatch deadlock

我需要澄清dispatch_queue与重入和死锁的关系。

阅读此博文Thread Safety Basics on iOS/OS X,我遇到了这句话:

  

所有调度队列都是不可重入的,这意味着如果你将崩溃   您尝试在当前队列上调度_sync。

那么,重入和死锁之间的关系是什么?如果dispatch_queue不可重入,为什么在使用dispatch_sync电话时会出现死锁?

根据我的理解,只有当您运行的线程与块发送到的线程相同时,才能使用dispatch_sync进行死锁。

一个简单的例子如下。如果我在主线程中运行代码,因为dispatch_get_main_queue()也会抓住主线程,我将陷入僵局。

dispatch_sync(dispatch_get_main_queue(), ^{

    NSLog(@"Deadlock!!!");

});

有任何澄清吗?

1 个答案:

答案 0 :(得分:12)

  
    

所有调度队列都是不可重入的,这意味着如果你将崩溃     您尝试在当前队列上调度_sync。

  
     

那么,重入和死锁之间的关系是什么?为什么,如果   dispatch_queue是不可重入的,当你出现时会出现死锁   使用dispatch_sync调用?

如果没有阅读那篇文章,我认为该陈述是针对串行队列的,因为它是错误的。

现在,让我们考虑一下分派队列如何工作的简化概念视图(在一些伪造的伪语言中)。我们还假设一个串行队列,而不考虑目标队列。

调度队列

当您创建一个调度队列时,基本上您会得到一个FIFO队列,一个简单的数据结构,您可以在其中推送对象,并从前面取出对象。

您还可以使用一些复杂的机制来管理线程池并进行同步,但大部分都是为了提高性能。让我们假设你还得到一个只运行无限循环的线程,处理来自队列的消息。

void processQueue(queue) {
    for (;;) {
        waitUntilQueueIsNotEmptyInAThreadSaveManner(queue)
        block = removeFirstObject(queue);
        block();
    }
}

dispatch_async

dispatch_async采取同样简单的观点会产生类似这样的东西......

void dispatch_async(queue, block) {
    appendToEndInAThreadSafeManner(queue, block);
}

所有它真正做的就是获取块,并将其添加到队列中。这就是它立即返回的原因,它只是将块添加到数据结构的末尾。在某些时候,其他线程会将此块从队列中拉出来并执行它。

请注意,这是FIFO保证发挥作用的地方。线程拉出队列并执行它们总是按照它们放在队列中的顺序。然后等待该块完全执行,然后从队列中取出下一个块

dispatch_sync

现在,另一个简单的dispatch_sync视图。在这种情况下,API保证它将在块返回之前等待块运行完成。特别是,调用此函数不会违反FIFO保证。

void dispatch_sync(queue, block) {
    bool done = false;
    dispatch_async(queue, { block(); done = true; });
    while (!done) { }
}

现在,这实际上是用信号量完成的,因此没有cpu循环和布尔标志,并且它没有使用单独的块,但我们试图保持简单。你应该明白这个想法。

该块被放置在队列中,然后该函数等待,直到它确定"另一个线程"已完成该块的运行。

重入

现在,我们可以通过多种不同方式获得可重入的呼叫。让我们考虑一下最明显的。

block1 = {
    dispatch_sync(queue, block2);
}
dispatch_sync(queue, block1);

这会将block1放在队列中,并等待它运行。最终处理队列的线程将关闭block1并开始执行它。当block1执行时,它会将block2放在队列中,然后等待它完成执行。

这是可重入的一个含义:当您从另一个dispatch_sync

的电话中重新打电话给dispatch_async

重新进入dispatch_sync

的死锁

但是,block1现在在队列内运行for循环。该代码正在执行block1,并且在block1完成之前不再处理队列中的任何内容。

然而,Block1已将block2放在队列中,并等待它完成。 Block2确实已被放置在队列中,但它永远不会被执行。 Block1是#34;等待"对于block2来完成,但是block2正在一个队列中,并且将它从队列中拉出并执行它的代码将不会运行直到block1完成。

来自NOT重新进入dispatch_sync

的死锁

现在,如果我们将代码更改为此...

block1 = {
    dispatch_sync(queue, block2);
}
dispatch_async(queue, block1);

我们在技术上没有重新进入dispatch_sync。但是,我们仍然有相同的情况,只是启动block1的线程没有等待它完成。

我们仍在运行block1,等待block2完成,但是运行block2的线程必须先用block1完成。这将永远不会发生,因为处理block1的代码正在等待block2从队列中取出并执行。

因此,调度队列的重入不是技术上重新输入相同的函数,而是重新进入相​​同的队列处理。

完全没有重新进入队列的死锁

在最简单的情况下(也是最常见的),让我们假设在主线程上调用[self foo],这是UI回调常见的。

-(void) foo {
    dispatch_sync(dispatch_get_main_queue(), ^{
        // Never gets here
    });
}

这不会重新进入"调度队列API,但它具有相同的效果。我们正在主线程上运行。主线程是将块从主队列中取出并进行处理的位置。主线程当前正在执行foo,并且一个块放在主队列上,然后foo等待该块执行。但是,它只能从队列中取出并在主线程完成其当前工作后执行。

这永远不会发生,因为主线程在foo完成之前不会进展,但是在它等待运行之前它永远不会完成......这不会发生。

  

据我了解,您只能使用dispatch_sync进行死锁   如果您运行的线程与块所在的线程相同   派遣到。

如前面的例子所示,情况并非如此。

此外,还有其他类似的场景,但不是那么明显,尤其是当sync访问隐藏在方法调用层中时。

避免死锁

避免死锁的唯一可靠方法是永远不要致电dispatch_sync(这不完全正确,但它足够接近)。如果将队列公开给用户,则尤其如此。

如果您使用自包含队列并控制其使用和目标队列,则可以在使用dispatch_sync时保持一定的控制权。

确实,dispatch_sync在串行队列中有一些有效的用途,但大多数可能是不明智的,只有在您确定不会同步时才会这样做。 ;访问相同或另一个资源(后者被称为致命的拥抱)。

修改

  

Jody,非常感谢您的回答。我真的了解你的全部   东西。我想提出更多要点......但是现在我不能。做   你有任何好的技巧,以便在引擎盖下学到这些东西吗? -   洛伦佐B。

不幸的是,我所见过的关于GCD的唯一书籍不是很先进。关于如何将它用于简单的一般用例(我猜这是大众市场书本应该做的事情),他们会仔细阅读简单的表面层次。

然而,GCD是开源的。 Here is the webpage for it,其中包含指向其svn和git存储库的链接。但是,网页看起来很旧(2010年),我不确定代码的最新状态。最近对git存储库的提交日期是2012年8月9日。

我确定最近有更新;但不确定它们会在哪里。

无论如何,我怀疑代码的概念框架多年来发生了很大的变化。

此外,调度队列的一般概念并不新鲜,并且已经存在很长时间了。

很多以前,我花了几天(和晚上)编写内核代码(处理我们认为是SVR4的第一个对称多处理实现),然后当我最终破坏内核时,我花了大部分时间我编写SVR4 STREAMS驱动程序的时间(由用户空间库包装)。最终,我完全进入了用户空间,并构建了一些最初的HFT系统(虽然当时没有调用它)。

调度队列概念在每一点都很普遍。它作为一个普遍可用的用户空间库的出现只是一个近期的发展。

编辑#2

  

Jody,谢谢你的编辑。所以,回顾一下串行调度队列是   不可重入,因为它可能产生无效状态(死锁)。   相反,可重入函数不会产生它。我对吗?    - Lorenzo B。

我想你可以这么说,因为它不支持可重入的电话。

但是,我想我更愿意说死锁是防止无效状态的结果。如果发生任何其他情况,则状态将受到损害,或者违反队列的定义。

Core Data' performBlockAndWait

考虑-[NSManagedObjectContext performBlockAndWait]。它是非异步的, 可重入。它在队列访问周围散布着一些小精灵粉尘,这样当第二个块从队列中调用时立即运行。"因此,它具有我上面描述的特征。

[moc performBlock:^{
    [moc performBlockAndWait:^{
        // This block runs immediately, and to completion before returning
        // However, `dispatch_async`/`dispatch_sync` would deadlock
    }];
}];

上面的代码没有产生死锁"来自重入(但API不能完全避免死锁)。

但是,根据您与谁交谈,执行此操作可能会产生无效(或不可预测/意外)状态。在这个简单的例子中,它清楚地知道发生了什么,但在更复杂的部分,它可能更加阴险。

至少,你必须非常小心你在performBlockAndWait内做了什么。

现在,实际上,这只是主队列MOC的一个真正问题,因为主运行循环正在主队列上运行,因此performBlockAndWait识别出并立即执行该块。但是,大多数应用程序都将MOC连接到主队列,并响应主队列上的用户保存事件。

如果您想观察调度队列如何与主运行循环交互,您可以在主运行循环上安装CFRunLoopObserver,并观察它如何处理主运行循环中的各种输入源。

如果你从未这样做过,那么这是一个有趣且有教育意义的实验(尽管你不能假设你所观察到的一切都是那样)。

无论如何,我通常会尽量避免使用dispatch_syncperformBlockAndWait