Lor*_*o B 2 macos multithreading deadlock grand-central-dispatch ios
我需要澄清dispatch_queues与reentrancy和死锁的关系.
在iOS/OS X上阅读此博客帖子Thread Safety Basics时,我遇到了这样一句话:
所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度_sync,则会出现死锁.
那么,重入和死锁之间的关系是什么?为什么,如果a dispatch_queue是不可重入的,当您使用dispatch_sync呼叫时会出现死锁?
根据我的理解,dispatch_sync只有当您运行的线程与块发送到的线程相同时,才能使用死锁.
一个简单的例子如下.如果我在主线程中运行代码,因为它dispatch_get_main_queue()也会抓住主线程并且我将以死锁结束.
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"Deadlock!!!");
});
Run Code Online (Sandbox Code Playgroud)
有任何澄清吗?
Jod*_*ins 12
所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度_sync,则会出现死锁.
那么,重入和死锁之间的关系是什么?如果dispatch_queue不可重入,为什么在使用dispatch_sync调用时会出现死锁?
在没有阅读那篇文章的情况下,我想这个陈述是关于串行队列的,因为它是错误的.
现在,让我们考虑一下分派队列如何工作的简化概念视图(在一些伪造的伪语言中).我们还假设一个串行队列,不考虑目标队列.
创建调度队列时,基本上您会获得一个FIFO队列,一个简单的数据结构,您可以在其中推送对象,并从前面获取对象.
您还可以获得一些复杂的机制来管理线程池并进行同步,但大多数都是为了提高性能.让我们假设您还获得了一个只运行无限循环的线程,处理来自队列的消息.
void processQueue(queue) {
for (;;) {
waitUntilQueueIsNotEmptyInAThreadSaveManner(queue)
block = removeFirstObject(queue);
block();
}
}
Run Code Online (Sandbox Code Playgroud)
对dispatch_async收益率采取同样简单的看法......
void dispatch_async(queue, block) {
appendToEndInAThreadSafeManner(queue, block);
}
Run Code Online (Sandbox Code Playgroud)
所有它真正做的就是获取块,并将其添加到队列中.这就是它立即返回的原因,它只是将块添加到数据结构的末尾.在某些时候,其他线程会将此块从队列中拉出来并执行它.
请注意,这是FIFO保证发挥作用的地方.线程拉出队列并执行它们总是按照它们放在队列中的顺序.然后等待直到该块完全执行,然后将下一个块从队列中移除
现在,另一个简单的观点dispatch_sync.在这种情况下,API保证它将在块返回之前等待块运行完成.特别是,调用此函数不会违反FIFO保证.
void dispatch_sync(queue, block) {
bool done = false;
dispatch_async(queue, { block(); done = true; });
while (!done) { }
}
Run Code Online (Sandbox Code Playgroud)
现在,这实际上是用信号量完成的,因此没有cpu循环和布尔标志,并且它不使用单独的块,但我们试图保持简单.你应该明白这个想法.
该块被放置在队列中,然后该函数等待,直到它确定"另一个线程"已经运行该块完成.
现在,我们可以通过多种不同的方式获得可重入的呼叫.让我们考虑最明显的.
block1 = {
dispatch_sync(queue, block2);
}
dispatch_sync(queue, block1);
Run Code Online (Sandbox Code Playgroud)
这会将block1放在队列中,并等待它运行.最终处理队列的线程将关闭block1并开始执行它.当block1执行时,它会将block2放在队列中,然后等待它完成执行.
这是重入的一个含义:当您dispatch_sync从另一个呼叫重新进入呼叫时dispatch_async
dispatch_sync但是,block1现在在队列的for循环中运行.该代码正在执行block1,并且在block1完成之前不会再处理队列中的任何内容.
但是,Block1已将block2放在队列中,并等待它完成.Block2确实已被放置在队列中,但它永远不会被执行.Block1正在"等待"block2完成,但是block2正在一个队列中,并且将它从队列中拉出并执行它的代码将不会运行直到block1完成.
dispatch_sync现在,如果我们将代码改为此...
block1 = {
dispatch_sync(queue, block2);
}
dispatch_async(queue, block1);
Run Code Online (Sandbox Code Playgroud)
我们在技术上没有重新进入dispatch_sync.但是,我们仍然有相同的场景,只是启动block1的线程没有等待它完成.
我们仍在运行block1,等待block2完成,但是运行block2的线程必须先用block1完成.这将永远不会发生,因为处理block1的代码正在等待block2从队列中取出并执行.
因此,调度队列的重入不是技术上重新输入相同的功能,而是重新进入相同的队列处理.
在最简单的情况下(也是最常见的),让我们假设[self foo]在主线程上调用,就像UI回调一样.
-(void) foo {
dispatch_sync(dispatch_get_main_queue(), ^{
// Never gets here
});
}
Run Code Online (Sandbox Code Playgroud)
这不会"重新进入"调度队列API,但它具有相同的效果.我们正在主线程上运行.主线程是将块从主队列中取出并进行处理的位置.主线程当前正在执行,foo并且在主队列上放置一个块,foo然后等待该块执行.但是,它只能从队列中取出并在主线程完成其当前工作后执行.
这永远不会发生,因为主线程在`foo完成之前不会进展,但它永远不会完成,直到它等待运行的那个块......这不会发生.
根据我的理解,只有当您运行的线程与块分派到的线程相同时,才能使用dispatch_sync进行死锁.
如前面的例子所示,情况并非如此.
此外,还有其他类似的场景,但不是那么明显,特别是当sync访问隐藏在方法调用的层中时.
避免死锁的唯一可靠方法是永远不要调用dispatch_sync(这不完全正确,但它足够接近).如果将队列公开给用户,则尤其如此.
如果使用自包含队列并控制其使用和目标队列,则可以在使用时保持一些控制dispatch_sync.
事实上,dispatch_sync在串行队列上有一些有效的用途,但大多数可能是不明智的,只有当你确定你不会"同步"访问相同或另一个资源时才会这样做(后者被称为致命的拥抱).
Jody,非常感谢您的回答.我真的了解你所有的东西.我想提出更多要点......但是现在我不能.你有任何好的技巧,以便在引擎盖下学习这些东西吗? - 洛伦佐B.
不幸的是,我见过的关于GCD的唯一书籍并不是很先进.关于如何将它用于简单的一般用例(我猜这是大众市场书本应该做的),他们会简单地介绍一下这些简单的表面层次.
但是,GCD是开源的. 这是它的网页,其中包含指向其svn和git存储库的链接.但是,网页看起来很旧(2010年),我不确定代码的最新状态.最近对git存储库的提交日期是2012年8月9日.
我确信最近有更新; 但不知道他们会在哪里.
无论如何,我怀疑代码的概念框架多年来发生了很大的变化.
此外,调度队列的一般概念并不新鲜,并且已经存在很长时间的许多形式.
很多时候,我花了几天(和晚上)编写内核代码(研究我们认为是SVR4的第一个对称多处理实现),然后当我最终破坏内核时,我花了大部分时间写作SVR4 STREAMS驱动程序(由用户空间库包装).最终,我完全进入了用户空间,并构建了一些最初的HFT系统(虽然当时没有调用它).
调度队列概念在每一点都很普遍.它的出现是一个普遍可用的用户空间库,这只是一个近期的发展.
Jody,谢谢你的编辑.因此,回顾一下串行调度队列是不可重入的,因为它可能产生无效状态(死锁).相反,可重入函数不会产生它.我对吗? - 洛伦佐B.
我想你可以这么说,因为它不支持可重入的调用.
但是,我想我更愿意说死锁是防止无效状态的结果.如果发生任何其他情况,则状态将被破坏,或者违反队列的定义.
performBlockAndWait考虑-[NSManagedObjectContext performBlockAndWait].它是非异步的,并且是可重入的.它在队列访问周围散布了一些小精灵灰尘,以便第二个块在从"队列"调用时立即运行.因此,它具有我在上面描述的特征.
[moc performBlock:^{
[moc performBlockAndWait:^{
// This block runs immediately, and to completion before returning
// However, `dispatch_async`/`dispatch_sync` would deadlock
}];
}];
Run Code Online (Sandbox Code Playgroud)
上面的代码不会因重入而"产生死锁"(但API无法完全避免死锁).
但是,根据您与谁交谈,执行此操作可能会产生无效(或不可预测/意外)状态.在这个简单的例子中,很明显发生了什么,但在更复杂的部分,它可能更加阴险.
至少,你必须非常小心你在里面做什么performBlockAndWait.
现在,实际上,这只是主队列MOC的一个真正问题,因为主运行循环正在主队列上运行,因此performBlockAndWait识别并立即执行该块.但是,大多数应用程序都将MOC连接到主队列,并响应主队列上的用户保存事件.
如果您想观察调度队列如何与主运行循环交互,您可以CFRunLoopObserver在主运行循环上安装一个,并观察它如何处理主运行循环中的各种输入源.
如果你从未这样做过,这是一个有趣且有教育意义的实验(尽管你不能假设你所观察到的将永远是那样).
无论如何,我通常会尽量避免dispatch_sync和performBlockAndWait.
| 归档时间: |
|
| 查看次数: |
1066 次 |
| 最近记录: |