ios - 澄清dispatch_queue,重入和死锁

标签 ios multithreading macos grand-central-dispatch deadlock

我需要澄清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)该块的线程相同时,才可以使用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_syncperformBlockAndWait

关于ios - 澄清dispatch_queue,重入和死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33543263/

相关文章:

ios - 为什么当我添加新语言时,我的主 Storyboard 没有用于本地化?

multithreading - 任务与流程,真的有区别吗?

Java : Out Of Memory Error when my application runs for longer time

objective-c - Hook C 函数

ruby-on-rails-3 - 适用于 Mac 的 Rails 代码重构工具

iOS:如果我将 View Controller 实例传递给 dispatch_async block ,它不会被释放

iOS:从 AWS SNS 向 APNS 发送推送通知

c++ - 如何休眠 APR 线程?

objective-c - 以编程方式禁用 Mac 上的所有通知中心横幅

ios - showViewController 方法只显示模态视图