- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我需要澄清dispatch_queue
与重入和死锁的关系。
在阅读此博客文章Thread Safety Basics on iOS/OS X时,我遇到了这句话:
All dispatch queues are non-reentrant, meaning you will deadlock if you attempt to dispatch_sync on the current queue.
dispatch_queue
是不可重入的,为什么在使用
dispatch_sync
调用时会出现死锁?
dispatch_sync
产生死锁。
dispatch_get_main_queue()
也将捕获主线程,并且我将陷入死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"Deadlock!!!");
});
最佳答案
All dispatch queues are non-reentrant, meaning you will deadlock if you attempt to dispatch_sync on the current queue.
那么,重入与死锁之间是什么关系呢?为什么,如果
dispatch_queue是不可重入的,当您处于
使用dispatch_sync调用?
无需阅读该文章,我可以想象该语句是引用串行队列的,因为否则它是错误的。
现在,让我们考虑一下分派(dispatch)队列如何工作的简化概念 View (以某些伪造的伪语言)。我们还假设一个串行队列,并且不考虑目标队列。
调度队列
创建调度队列时,基本上会得到一个FIFO队列,这是一个简单的数据结构,您可以在其中插入对象,然后从前移对象。
您还可以获得一些复杂的机制来管理线程池和进行同步,但是大多数机制都是为了提高性能。让我们简单地假设您还获得一个线程,该线程仅运行无限循环,即可处理队列中的消息。
void processQueue(queue) {
for (;;) {
waitUntilQueueIsNotEmptyInAThreadSaveManner(queue)
block = removeFirstObject(queue);
block();
}
}
dispatch_async
采用dispatch_async
的相同简化 View 会产生类似以下内容...
void dispatch_async(queue, block) {
appendToEndInAThreadSafeManner(queue, block);
}
它真正要做的只是获取块并将其添加到队列中。这就是为什么它立即返回的原因,它只是将块添加到数据结构的末尾。在某个时候,另一个线程会将这个块从队列中拉出并执行。
注意,这就是FIFO保证发挥作用的地方。线程拉出队列,并始终按放置在队列中的顺序执行它们。然后等待直到该块完全执行,然后再将下一个块从队列中取出。
dispatch_sync
现在,是dispatch_sync
的另一种简化 View 。在这种情况下,API保证它会等到块运行完毕才能返回。特别是,调用此函数不会违反FIFO保证。
void dispatch_sync(queue, block) {
bool done = false;
dispatch_async(queue, { block(); done = true; });
while (!done) { }
}
现在,这实际上是通过信号量完成的,因此没有cpu循环和 bool 标志,并且它不使用单独的块,但是我们试图使其保持简单。您应该知道这个主意。
将该块放置在队列中,然后函数等待,直到确定“其他线程”已将该块运行完毕为止。
再入
现在,我们可以通过多种不同的方式获得重入调用。让我们考虑最明显的。
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完成之前不会继续运行,但是直到等待运行的那个块才永远不会完成……这不会发生。
In my understanding, you can have a deadlock using dispatch_sync only if the thread you are running on is the same thread where the block is dispatch into.
如上述示例所示,情况并非如此。
此外,还有其他一些相似但不太明显的场景,尤其是当sync
访问隐藏在方法调用层中时。
避免死锁
避免死锁的唯一肯定方法是永远不要调用dispatch_sync
(虽然不完全正确,但是已经足够接近了)。如果您向用户公开队列,则尤其如此。
如果您使用独立队列,并控制其使用队列和目标队列,则可以在使用dispatch_sync
时保持一些控制。
的确,串行队列上确实有dispatch_sync
的一些有效用法,但是大多数可能是不明智的,只有当您确定您不会“同步”访问相同或另一个资源(已知后者)时,才应这样做。作为致命的拥抱)。
编辑
Jody, Thanks a lot for your answer. I really understood all of your stuff. I would like to put more points...but right now I cannot. 😢 Do you have any good tips in order to learn this under the hood stuff? – Lorenzo B.
不幸的是,我所见过的仅有的关于GCD的书并不十分先进。他们介绍了有关如何将其用于简单的通用用例的简单表面知识(我想这是大众市场书应该做的事情)。
但是,GCD是开源的。 Here is the webpage for it,包括指向其svn和git存储库的链接。但是,该网页看起来很旧(2010),我不确定代码的最新程度。对git存储库的最新提交日期为2012年8月9日。
我确定会有更多最新更新;但不确定它们会在哪里。
无论如何,我怀疑这些年来代码的概念框架已经发生了很大变化。
另外,调度队列的一般概念并不是什么新鲜事物,并且已经以很多种形式存在了很长时间。
许多个月前,我花了几天(甚至晚上)来编写内核代码(致力于我们认为是SVR4的第一个对称多处理实现),然后当我最终突破内核时,我将大部分时间都花在了编写上。 SVR4 STREAMS驱动程序(由用户空间库包装)。最终,我将其完全引入了用户空间,并构建了一些最早的HFT系统(尽管当时并没有这样称呼)。
调度队列的概念在每个方面都很普遍。它作为通用的用户空间库的出现只是最近的发展。
编辑#2
Jody, thanks for your edit. So, to recap a serial dispatch queue is not reentrant since it could produce an invalid state (a deadlock). On the contrary, an reentrant function will not produce it. Am I right? – 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
。
关于ios - 澄清dispatch_queue,重入和死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33543263/
我有类似下面的代码: ... id: myComponent signal updateState() property variant modelList: [] Repeater { mo
我正在处理一些我无法展示的私有(private)代码,但我已经制作了一些示例代码来描述我的问题: 主.c: #include #include #include #include typede
这个问题在这里已经有了答案: 关闭10 年前。 Possible Duplicate: what are the differences in die() and exit() in PHP? 我想
在编写 Perl 模块时,在模块内部使用 croak/die 是一个好习惯吗? 毕竟,如果调用者不使用 eval block ,模块可能会使调用它的程序崩溃。 在这些情况下,最佳做法是什么? 最佳答案
我有一些搜索线程正在存储结果。我知道当线程启动时,JVM native 代码会代理在操作系统上创建新 native 线程的请求。这需要 JVM 之外的一些内存。当线程终止并且我保留对它的引用并将其用作
我刚刚花了很多时间调试一个我追溯到 wantarray() 的问题。 .我已将其提炼为这个测试用例。 (忽略 $! 在这种情况下不会有任何有用信息的事实)。我想知道为什么wantarray在第二个示例
我看到一些代码是这样做的: if(something){ echo 'exit from program'; die; } ...more code 和其他只使用 die 的人: if
我正在尝试将此表格用于: 如果任何 $_POST 变量等于任何其他 $_POST 变量抛出错误。 如果只有几个,那不是问题,但我有大约 20 个左右所以如果我想这样做,我将不得不像这样 但这
每次我运行: hadoop dfsadmin -report 我得到以下输出: Configured Capacity: 0 (0 KB) Present Capacity: 0 (0 KB) DFS
我是一名优秀的程序员,十分优秀!