澄清dispatch_queue,重入和死锁

我需要澄清dispatch_queue是如何与重入和死锁有关的。

阅读这篇博客文章, 在iOS / OS X上的线程安全基础知识 ,我遇到了这样一句话:

所有分派队列都是不可重入的,这意味着如果您尝试在当前队列上分派同步,则会发生死锁。

那么,重入和僵局之间的关系是什么呢? 为什么,如果一个dispatch_queue是不可重入的,当你使用dispatch_sync调用时会产生死锁?

在我的理解中,只有当你正在运行的线程与调度块所在的线程相同时,才可以使用dispatch_sync产生死锁。

一个简单的例子如下。 如果我在主线程中运行代码,由于dispatch_get_main_queue()也会占用主线程,所以我将以死锁结束。

 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"Deadlock!!!"); }); 

任何澄清?

所有分派队列都是不可重入的,这意味着如果您尝试在当前队列上分派同步,则会发生死锁。

那么,重入和僵局之间的关系是什么呢? 为什么,如果一个dispatch_queue是不可重入的,当你使用dispatch_sync调用时会产生死锁?

没有阅读过这篇文章,我想这个陈述是关于串行队列的,因为它是错误的。

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

调度队列

当你创build一个分派队列时,基本上你会得到一个FIFO队列,一个简单的数据结构,你可以在这个末端推送对象,并从前面拿走对象。

您还可以获得一些复杂的机制来pipe理线程池并进行同步,但大部分是为了性能。 让我们简单地假设你也得到一个线程,它只是运行一个无限循环,处理队列中的消息。

 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放在队列中,并等待它运行。 最终处理队列的线程将closuresblock1,并开始执行它。 当block1执行时,它会将block2放在队列中,然后等待它完成执行。

这是重入的一个含义:当你重新input一个调用dispatch_sync从另一个调用dispatch_async

重新进入dispatch_sync死锁

但是,block1现在正在队列的for循环中运行。 该代码正在执行block1,并且在block1完成之前不会处理更多的队列。

不过,Block1已将block2放在队列中,并正在等待它完成。 Block2确实已经放在队列中,但是它永远不会被执行。 Block1正在等待block2完成,但block2正坐在一个队列中,并且将其从队列中拉出并执行的代码将不会运行,直到block1完成。

从NOT重新进入dispatch_sync死锁

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

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

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

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

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

从NOT重新进入队列的死锁

在最简单的情况下(也是最常见的),让我们假设在主线程上调用[self foo] ,就像UIcallback中常见的那样。

 -(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是开源的。 这里是它的网页 ,其中包括他们的svn和git存储库的链接。 然而,网页看起来很老(2010),我不知道代码是多么新近。 最近对git仓库的提交date为2012年8月9日。

我相信有更新的更新; 但不知道他们会在哪里。

无论如何,我怀疑这些守则的概念框架在过去几年有了很大的变化。

而且,调度队列的总体思路并不新鲜,而且已经有很长一段时间了。

许多月前,我花了我的时间(和夜晚)编写内核代码(工作在我们认为是SVR4的第一个对称多处理实现),然后当我最终破坏内核时,我花了大部分时间写SVR4 STREAMS驱动程序(由用户空间库包装)。 最终,我完全进入了用户空间,并构build了一些最初的HFT系统(尽pipe当时并没有这样做)。

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

编辑#2

Jody,谢谢你的修改。 因此,重述一个串行调度队列是不可重入的,因为它可能产生一个无效状态(死锁)。 相反,重入函数不会产生它。 我对吗? – 洛伦佐B.

我想你可以这样说,因为它不支持可重入呼叫。

不过,我想我宁愿说死锁是防止无效状态的结果。 如果还有其他事情发生,那么状态将会被破坏,或者队列的定义将被违反。

核心数据的performBlockAndWait

考虑-[NSManagedObjectContext performBlockAndWait] 。 它是非asynchronous的,并且可重入的。 它在队列访问周围喷洒了一些小精灵粉尘,以便当从“队列”调用时,第二个块立即运行。 因此,它具有我上面描述的特征。

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

上面的代码不会从重入(reentrancy)中产生“死锁”(但API无法完全避免死锁)。

但是,根据您与谁交谈,这样做可能会产生无效(或不可预知/意外)状态。 在这个简单的例子中,很清楚发生了什么,但是在更复杂的部分,它可能更隐蔽。

至less,你必须非常小心你在performBlockAndWait里面做了什么。

现在,在实践中,这对于主队列MOC来说只是一个真正的问题,因为主运行循环在主队列上运行,所以performBlockAndWait识别并立即执行该块。 但是,大多数应用程序都将MOC附加到主队列,并响应主队列上的用户保存事件。

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

如果你从来没有这样做,这是一个有趣的和教育性的实验(虽然你不能假设你所观察到的将永远是这样的)。

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

Interesting Posts