我需要澄清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!!!");
});
有任何澄清吗?
答案 0 :(得分:12)
所有调度队列都是不可重入的,这意味着如果你将崩溃 您尝试在当前队列上调度_sync。
那么,重入和死锁之间的关系是什么?为什么,如果 dispatch_queue是不可重入的,当你出现时会出现死锁 使用dispatch_sync调用?
如果没有阅读那篇文章,我认为该陈述是针对串行队列的,因为它是错误的。
现在,让我们考虑一下分派队列如何工作的简化概念视图(在一些伪造的伪语言中)。我们还假设一个串行队列,而不考虑目标队列。
当您创建一个调度队列时,基本上您会得到一个FIFO队列,一个简单的数据结构,您可以在其中推送对象,并从前面取出对象。
您还可以使用一些复杂的机制来管理线程池并进行同步,但大部分都是为了提高性能。让我们假设你还得到一个只运行无限循环的线程,处理来自队列的消息。
void processQueue(queue) {
for (;;) {
waitUntilQueueIsNotEmptyInAThreadSaveManner(queue)
block = removeFirstObject(queue);
block();
}
}
对dispatch_async
采取同样简单的观点会产生类似这样的东西......
void dispatch_async(queue, block) {
appendToEndInAThreadSafeManner(queue, block);
}
所有它真正做的就是获取块,并将其添加到队列中。这就是它立即返回的原因,它只是将块添加到数据结构的末尾。在某些时候,其他线程会将此块从队列中拉出来并执行它。
请注意,这是FIFO保证发挥作用的地方。线程拉出队列并执行它们总是按照它们放在队列中的顺序。然后等待该块完全执行,然后从队列中取出下一个块
现在,另一个简单的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完成。
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系统(虽然当时没有调用它)。
调度队列概念在每一点都很普遍。它作为一个普遍可用的用户空间库的出现只是一个近期的发展。
Jody,谢谢你的编辑。所以,回顾一下串行调度队列是 不可重入,因为它可能产生无效状态(死锁)。 相反,可重入函数不会产生它。我对吗? - Lorenzo B。
我想你可以这么说,因为它不支持可重入的电话。
但是,我想我更愿意说死锁是防止无效状态的结果。如果发生任何其他情况,则状态将受到损害,或者违反队列的定义。
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_sync
和performBlockAndWait
。