问题描述
我需要澄清dispatch_queue
s与重入性和死锁的关系。
阅读这篇博客文章Thread Safety Basics on 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调用?在没有读过那篇文章的情况下,我认为该语句引用的是串行队列,因为它在其他方面是假的。
现在,让我们考虑调度队列如何工作的简化概念性视图(使用一些虚构的伪语言)。我们还假设为串行队列,不考虑目标队列。调度队列
当您创建调度队列时,基本上会得到一个FIFO队列,这是一个简单的数据结构,您可以在其中将对象推到末尾,并将对象从前面移走。
您还可以使用一些复杂的机制来管理线程池和进行同步,但这主要是为了提高性能。让我们简单地假设您还有一个线程,它只运行一个无限循环,处理队列中的消息。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放在队列中,并等待它运行。最终,处理队列的线程将弹出Block1,并开始执行它。当Block1执行时,它将把Block2放在队列中,然后等待它完成执行。
这是可重入性的一个含义:当您从对dispatch_sync
的另一个调用重新输入对dispatch_sync
的调用
重新进入导致死锁dispatch_sync
但是,Block1现在正在队列的for循环中运行。该代码正在执行Block1,并且在Block1完成之前不会处理队列中的任何内容。但是,Block1已将块2放入队列,并正在等待其完成。Block2确实已被放入队列,但它永远不会被执行。Block1正在"等待"Block2完成,但Block2位于队列中,在Block1完成之前,将其从队列中拉出并执行它的代码将不会运行。未重新进入导致死锁dispatch_sync
现在,如果我们将代码更改为...
block1 = {
dispatch_sync(queue, block2);
}
dispatch_async(queue, block1);
从技术上讲,我们不会重新进入dispatch_sync
。但是,我们仍然有相同的场景,只是启动Block1的线程并没有等待它完成。
因此,调度队列的可重入性在技术上不是重新进入相同的功能,而是重新进入相同的队列处理。
根本不重新进入队列导致的死锁
在最简单(也是最常见)的情况下,我们假设[self foo]
在主线程上被调用,这在UI回调中很常见。
-(void) foo {
dispatch_sync(dispatch_get_main_queue(), ^{
// Never gets here
});
}
这不会"重新进入"调度队列API,但它具有相同的效果。我们在主线上运行。主线程是将块从主队列中取出并进行处理的地方。主线程当前正在执行foo
,并将一个块放在主队列中,然后foo
等待该块被执行。但是,它只能在主线程完成其当前工作后从队列中移除并执行。
如前述示例所示,情况并非如此。
此外,还有其他类似的场景,但不是很明显,尤其是当sync
访问隐藏在方法调用层中时。
避免死锁
避免死锁的唯一可靠方法是永远不调用dispatch_sync
(这并不完全正确,但已经足够接近了)。如果您向用户公开您的队列,情况尤其如此。如果使用自包含队列并控制其使用和目标队列,则在使用dispatch_sync
时可以保持一定程度的控制。
确实,在序列队列上有一些dispatch_sync
的有效用法,但大多数用法可能是不明智的,只有在您确定不会‘同步’访问相同或另一个资源(后者称为致命拥抱)时才应该这样做。
编辑
不幸的是,我在GCD上看到的书都不是很高级。他们就如何将其用于简单的通用用例(我猜这是一本大众市场书应该做的事情)复习了一些简单的表层内容。
然而,GCD是开源的。Here is the webpage for it,其中包括指向其SVN和GIT存储库的链接。然而,网页看起来很旧(2010年),我不确定代码是多新的。最近一次提交GIT存储库的日期为2012年8月9日。
我确信有更新的更新;但不确定它们会在哪里。
无论如何,我怀疑这些年来代码的概念框架是否发生了很大变化。
另外,调度队列的一般概念并不新鲜,并且已经以多种形式存在了很长一段时间。
很多年前,我日以继夜地编写内核代码(我们认为这是SVR4的第一个对称多处理实现),然后当我最终破坏内核时,我花了大部分时间编写SVR4流驱动程序(由用户空间库包装)。最后,我把它完全放入了用户空间,并构建了一些最早的HFT系统(尽管当时还不是这样叫的)。调度队列的概念在其中的每一点都很流行。它作为一个普遍可用的用户空间库的出现只是最近的一个发展。
编辑#2
我猜你可以这么说,因为它不支持可重入调用。
然而,我认为我更愿意说死锁是防止无效状态的结果。如果发生其他情况,则可能会危及状态,或者会违反队列的定义。
核心数据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
中执行操作。
performBlockAndWait
认识到这一点并立即执行该块。但是,大多数应用程序都将MOC附加到主队列,并响应主队列上的用户保存事件。如果要查看调度队列如何与主运行循环交互,可以在主运行循环上安装CFRunLoopObserver
,并查看它如何处理主运行循环中的各种输入源。
如果你从来没有这样做过,这是一个有趣的、有教育意义的实验(尽管你不能假设你观察到的东西总是这样)。
反正我一般都尽量避开dispatch_sync
和performBlockAndWait
。
这篇关于关于DISPATCH_QUEUE、可重入和死锁的说明的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!