有关dispatch_queue,重入和死锁的说明

问题描述 投票:2回答:1

我需要澄清dispatch_queue与重入和死锁的关系。

阅读此博客文章Thread Safety Basics on iOS/OS X,我遇到了这句话:

所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度dispatch_sync。

那么,重入与死锁之间是什么关系?如果dispatch_queue是不可重入的,为什么在使用dispatch_sync调用时会出现死锁?

据我所知,只有当运行的线程与分派该块的线程相同时,才能使用dispatch_sync产生死锁。

一个简单的例子如下。如果我在主线程中运行代码,由于dispatch_get_main_queue()也将抓住主线程,并且我将陷入死锁。

dispatch_sync(dispatch_get_main_queue(), ^{

    NSLog(@"Deadlock!!!");

});

是否有澄清?

ios multithreading macos grand-central-dispatch deadlock
1个回答
13
投票

所有调度队列都是不可重入的,这意味着如果您尝试在当前队列上调度dispatch_sync。

那么,重入与死锁之间是什么关系?为什么,如果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已将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完成之前主线程不会继续执行,但是直到等待运行的那个块之前它永远不会完成...不会发生。

据我所知,您只能使用dispatch_sync产生死锁如果您正在运行的线程与该块所在的线程相同分派到。

如上述示例所示,不是这种情况。

此外,还有其他一些相似但不那么明显的场景,特别是当sync访问隐藏在方法调用层中时。

避免死锁

避免死锁的唯一确定的方法是永远不要调用dispatch_sync(虽然不完全正确,但是已经足够接近了)。如果您向用户公开队列,则尤其如此。

如果使用独立队列,并控制其使用队列和目标队列,则在使用dispatch_sync时可以保持某些控制。

实际上,在串行队列上确实有dispatch_sync的某些有效用法,但大多数可能是不明智的,只有当您确定您不会'同步'访问相同或另一资源时,才应这样做(后者被称为致命的拥抱)。

编辑

乔迪,非常感谢您的回答。我真的很了解你东西。我想提出更多要点...但是现在我不能。 😢做您有什么好的技巧可以在引擎盖下学习这些东西? –洛伦佐·B。

[不幸的是,我所见过的关于GCD的唯一书籍并不十分先进。他们讨论了有关如何将其用于简单的通用用例的简单表面层的东西(我想这是大众市场书应该做的)。

但是,GCD是开源的。 Here is the webpage for it,包括指向其svn和git存储库的链接。但是,该网页看起来很旧(2010),我不确定代码的最新程度。对git存储库的最新提交日期为2012年8月9日。

我确定还有更多最新更新;但不确定它们会在哪里。

无论如何,我怀疑代码的概念框架多年来已经发生了很大变化。

此外,调度队列的一般概念也不是新鲜事物,并且已经以很多种形式存在了很长时间。

[许多月以前,我花了几天和夜晚来写内核代码(致力于我们认为是SVR4的第一个对称多处理实现),然后当我最终违反内核时,我花了大部分时间编写SVR4 STREAMS驱动程序(由用户空间库包装)。最终,我将其完全引入了用户空间,并构建了一些最早的HFT系统(尽管当时并没有这样称呼)。

调度队列的概念在每个方面都很普遍。它作为通用的用户空间库的出现只是最近的发展。

编辑#2

乔迪,谢谢您的编辑。因此,回顾一下串行调度队列是不可重入,因为它可能产生无效状态(死锁)。相反,可重入函数不会产生它。我对吗?– 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

© www.soinside.com 2019 - 2024. All rights reserved.