我需要澄清dispatch_queue
与重入和死锁的关系。
阅读此博客文章Thread Safety Basics on iOS/OS X,我遇到了这句话:
所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度dispatch_sync。
那么,重入与死锁之间是什么关系?如果dispatch_queue
是不可重入的,为什么在使用dispatch_sync
调用时会出现死锁?
据我所知,只有当运行的线程与分派该块的线程相同时,才能使用dispatch_sync
产生死锁。
一个简单的例子如下。如果我在主线程中运行代码,由于dispatch_get_main_queue()
也将抓住主线程,并且我将陷入死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"Deadlock!!!");
});
是否有澄清?
所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度dispatch_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_sync
的呼叫时>
dispatch_sync
的僵局但是,block1现在正在队列的for循环内运行。该代码正在执行block1,并且在block1完成之前不会处理队列中的任何内容。
但是,block1已将block2放在队列中,并等待其完成。实际上,Block2已放置在队列中,但永远不会执行。 Block1正在“等待” 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
的某些有效用法,但大多数可能是不明智的,只有当您确定您不会'同步'访问相同或另一资源时,才应这样做(后者被称为致命的拥抱)。
乔迪,非常感谢您的回答。我真的很了解你东西。我想提出更多要点...但是现在我不能。 😢做您有什么好的技巧可以在引擎盖下学习这些东西? –洛伦佐·B。
[不幸的是,我所见过的关于GCD的唯一书籍并不十分先进。他们讨论了有关如何将其用于简单的通用用例的简单表面层的东西(我想这是大众市场书应该做的)。
但是,GCD是开源的。 Here is the webpage for it,包括指向其svn和git存储库的链接。但是,该网页看起来很旧(2010),我不确定代码的最新程度。对git存储库的最新提交日期为2012年8月9日。
我确定还有更多最新更新;但不确定它们会在哪里。
无论如何,我怀疑代码的概念框架多年来已经发生了很大变化。
此外,调度队列的一般概念也不是新鲜事物,并且已经以很多种形式存在了很长时间。
[许多月以前,我花了几天和夜晚来写内核代码(致力于我们认为是SVR4的第一个对称多处理实现),然后当我最终违反内核时,我花了大部分时间编写SVR4 STREAMS驱动程序(由用户空间库包装)。最终,我将其完全引入了用户空间,并构建了一些最早的HFT系统(尽管当时并没有这样称呼)。
调度队列的概念在每个方面都很普遍。它作为通用的用户空间库的出现只是最近的发展。
乔迪,谢谢您的编辑。因此,回顾一下串行调度队列是不可重入,因为它可能产生无效状态(死锁)。相反,可重入函数不会产生它。我对吗?– 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
。