关于 dispatch_queue、重入和死锁的说明
Clarifications on dispatch_queue, reentrancy and deadlocks
我需要澄清 dispatch_queue
s 与重入和死锁的关系。
读这篇博文postThread 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.
那么,重入和死锁是什么关系呢?为什么,如果
a 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);
}
它真正做的就是取出块,并将其添加到队列中。这就是为什么它 returns 立即,它只是将块添加到数据结构的末尾。在某个时候,另一个线程会将这个块从队列中拉出并执行它。
请注意,这就是 FIFO 保证发挥作用的地方。将块从队列中拉出并执行的线程总是按照它们在队列中的顺序执行。然后它会等到该块完全执行后再从队列中取出下一个块
dispatch_sync
现在,dispatch_sync
的另一个简单视图。在这种情况下,API 保证它将等到块 运行 在它 returns 之前完成。特别是,调用此函数不会违反 FIFO 保证。
void dispatch_sync(queue, block) {
bool done = false;
dispatch_async(queue, { block(); done = true; });
while (!done) { }
}
现在,这实际上是用信号量完成的,因此没有 cpu 循环和布尔标志,并且它不使用单独的块,但我们试图保持简单。你应该明白了。
块放在队列中,然后函数等待,直到它确定 "the other thread" 有 运行 块完成。
重入
现在,我们可以通过多种不同的方式获得可重入调用。让我们考虑最明显的。
block1 = {
dispatch_sync(queue, block2);
}
dispatch_sync(queue, block1);
这会将 block1 放入队列中,并等待它到达 运行。最终处理队列的线程将弹出 block1,并开始执行它。当block1执行时,会把block2放入队列,然后等待它执行完。
这是重入的一个含义:当您从另一个对 dispatch_sync
的调用重新进入对 dispatch_sync
的调用时
重新进入的死锁dispatch_sync
但是,block1 现在 运行ning 在队列的 for 循环中。该代码正在执行块 1,并且在块 1 完成之前不会处理队列中的任何内容。
但是,块 1 已将块 2 放入队列中,正在等待它完成。 Block2确实已经放入了队列,但是永远不会执行。块 1 "waiting" 块 2 完成,但块 2 位于队列中,将其从队列中拉出并执行它的代码不会 运行 直到块 1 完成。
无法重新进入的死锁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
});
}
这不是 "reenter" 调度队列 API,但它具有相同的效果。我们在主线程上 运行ning。主线程是从主队列中取出块并进行处理的地方。主线程当前正在执行 foo
并且一个块被放置在主队列中,然后 foo
等待该块被执行。但是,它只能在主线程完成当前工作后才能从队列中取出并执行。
这永远不会发生,因为主线程在 `foo 完成之前不会继续,但它永远不会完成,直到它正在等待 运行s 的块...这不会发生。
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
在串行队列上有一些有效的用途,但大多数可能是不明智的,只有当您确定自己不会 '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 驱动程序(由用户 space 库包装)。最终,我将它完全变成了用户 space,并构建了一些最早的 HFT 系统(尽管当时不叫它)。
调度队列的概念在其中很普遍。它作为一个普遍可用的用户出现 space 库只是最近才开发的。
编辑#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]
。它是非异步的,并且 是 可重入的。它在队列访问周围撒了一些精灵粉,因此当从 "the queue." 调用时,第二个块 运行 立即出现,因此,它具有我上面描述的特征。
[moc performBlock:^{
[moc performBlockAndWait:^{
// This block runs immediately, and to completion before returning
// However, `dispatch_async`/`dispatch_sync` would deadlock
}];
}];
以上代码没有"produce a deadlock"重入(但是API不能完全避免死锁)。
但是,根据您与谁交谈,这样做可能会产生无效(或 unpredictable/unexpected)状态。在这个简单的例子中,很清楚发生了什么,但在更复杂的部分,它可能更阴险。
最起码,你在performBlockAndWait
.
里面做的事情一定要非常小心
现在,实际上,这只是主队列 MOC 的一个真正问题,因为主 运行 循环在主队列上 运行ning,所以 performBlockAndWait
识别那并立即执行该块。但是,大多数应用程序都有一个附加到主队列的 MOC,并响应主队列上的用户保存事件。
如果你想观察调度队列如何与主运行循环交互,你可以在主运行循环上安装一个CFRunLoopObserver
,观察它如何处理各种主 运行 循环中的输入源。
如果您从未这样做过,那么这是一个有趣且具有教育意义的实验(尽管您不能假设您观察到的总是那样)。
无论如何,我通常尽量避免dispatch_sync
和performBlockAndWait
。
我需要澄清 dispatch_queue
s 与重入和死锁的关系。
读这篇博文postThread 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.
那么,重入和死锁是什么关系呢?为什么,如果 a 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);
}
它真正做的就是取出块,并将其添加到队列中。这就是为什么它 returns 立即,它只是将块添加到数据结构的末尾。在某个时候,另一个线程会将这个块从队列中拉出并执行它。
请注意,这就是 FIFO 保证发挥作用的地方。将块从队列中拉出并执行的线程总是按照它们在队列中的顺序执行。然后它会等到该块完全执行后再从队列中取出下一个块
dispatch_sync
现在,dispatch_sync
的另一个简单视图。在这种情况下,API 保证它将等到块 运行 在它 returns 之前完成。特别是,调用此函数不会违反 FIFO 保证。
void dispatch_sync(queue, block) {
bool done = false;
dispatch_async(queue, { block(); done = true; });
while (!done) { }
}
现在,这实际上是用信号量完成的,因此没有 cpu 循环和布尔标志,并且它不使用单独的块,但我们试图保持简单。你应该明白了。
块放在队列中,然后函数等待,直到它确定 "the other thread" 有 运行 块完成。
重入
现在,我们可以通过多种不同的方式获得可重入调用。让我们考虑最明显的。
block1 = {
dispatch_sync(queue, block2);
}
dispatch_sync(queue, block1);
这会将 block1 放入队列中,并等待它到达 运行。最终处理队列的线程将弹出 block1,并开始执行它。当block1执行时,会把block2放入队列,然后等待它执行完。
这是重入的一个含义:当您从另一个对 dispatch_sync
dispatch_sync
的调用时
重新进入的死锁dispatch_sync
但是,block1 现在 运行ning 在队列的 for 循环中。该代码正在执行块 1,并且在块 1 完成之前不会处理队列中的任何内容。
但是,块 1 已将块 2 放入队列中,正在等待它完成。 Block2确实已经放入了队列,但是永远不会执行。块 1 "waiting" 块 2 完成,但块 2 位于队列中,将其从队列中拉出并执行它的代码不会 运行 直到块 1 完成。
无法重新进入的死锁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
});
}
这不是 "reenter" 调度队列 API,但它具有相同的效果。我们在主线程上 运行ning。主线程是从主队列中取出块并进行处理的地方。主线程当前正在执行 foo
并且一个块被放置在主队列中,然后 foo
等待该块被执行。但是,它只能在主线程完成当前工作后才能从队列中取出并执行。
这永远不会发生,因为主线程在 `foo 完成之前不会继续,但它永远不会完成,直到它正在等待 运行s 的块...这不会发生。
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
在串行队列上有一些有效的用途,但大多数可能是不明智的,只有当您确定自己不会 '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 驱动程序(由用户 space 库包装)。最终,我将它完全变成了用户 space,并构建了一些最早的 HFT 系统(尽管当时不叫它)。
调度队列的概念在其中很普遍。它作为一个普遍可用的用户出现 space 库只是最近才开发的。
编辑#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]
。它是非异步的,并且 是 可重入的。它在队列访问周围撒了一些精灵粉,因此当从 "the queue." 调用时,第二个块 运行 立即出现,因此,它具有我上面描述的特征。
[moc performBlock:^{
[moc performBlockAndWait:^{
// This block runs immediately, and to completion before returning
// However, `dispatch_async`/`dispatch_sync` would deadlock
}];
}];
以上代码没有"produce a deadlock"重入(但是API不能完全避免死锁)。
但是,根据您与谁交谈,这样做可能会产生无效(或 unpredictable/unexpected)状态。在这个简单的例子中,很清楚发生了什么,但在更复杂的部分,它可能更阴险。
最起码,你在performBlockAndWait
.
现在,实际上,这只是主队列 MOC 的一个真正问题,因为主 运行 循环在主队列上 运行ning,所以 performBlockAndWait
识别那并立即执行该块。但是,大多数应用程序都有一个附加到主队列的 MOC,并响应主队列上的用户保存事件。
如果你想观察调度队列如何与主运行循环交互,你可以在主运行循环上安装一个CFRunLoopObserver
,观察它如何处理各种主 运行 循环中的输入源。
如果您从未这样做过,那么这是一个有趣且具有教育意义的实验(尽管您不能假设您观察到的总是那样)。
无论如何,我通常尽量避免dispatch_sync
和performBlockAndWait
。