60

我遇到了一个场景,我有一个委托回调,它可能发生在主线程或另一个线程上,直到运行时(使用StoreKit.framework)我才知道哪个。

我还有需要在回调中更新的 UI 代码,这需要在函数执行之前发生,所以我最初的想法是有一个这样的函数:

-(void) someDelegateCallback:(id) sender
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        // ui update code here
    });

    // code here that depends upon the UI getting updated
}

当它在后台线程上执行时,效果很好。但是,当在主线程上执行时,程序会陷入死锁。

仅这一点对我来说似乎很有趣,如果我dispatch_sync正确阅读了文档,那么我希望它直接执行该块,而不用担心将其安排到运行循环中,如此处所述

作为一种优化,此函数在可能的情况下调用当前线程上的块。

但是,这没什么大不了的,它只是意味着更多的打字,这导致我采用这种方法:

-(void) someDelegateCallBack:(id) sender
{
    dispatch_block_t onMain = ^{
        // update UI code here
    };

    if (dispatch_get_current_queue() == dispatch_get_main_queue())
       onMain();
    else
       dispatch_sync(dispatch_get_main_queue(), onMain);
}

然而,这似乎有点倒退。这是 GCD 制作中的错误,还是我在文档中遗漏了什么?

4

6 回答 6

75

dispatch_sync做两件事:

  1. 排队一个块
  2. 阻塞当前线程,直到该块完成运行

鉴于主线程是一个串行队列(这意味着它只使用一个线程),如果您在主队列上运行以下语句:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});

将发生以下事件:

  1. dispatch_sync将块放入主队列中。
  2. dispatch_sync阻塞主队列的线程,直到块完成执行。
  3. dispatch_sync永远等待,因为该块应该运行的线程被阻塞了。

理解这个问题的关键是dispatch_sync不执行块,它只是将它们排队。执行将在运行循环的未来迭代中发生。

以下方法:

if (queueA == dispatch_get_current_queue()){
    block();
} else {
    dispatch_sync(queueA, block);
}

很好,但请注意,它不会保护您免受涉及队列层次结构的复杂场景的影响。在这种情况下,当前队列可能与您尝试发送块的先前阻塞队列不同。例子:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        // dispatch_get_current_queue() is B, but A is blocked, 
        // so a dispatch_sync(A,b) will deadlock.
        dispatch_sync(queueA, ^{
            // some task
        });
    });
});

对于复杂的情况,在调度队列中读/写键值数据:

dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);

static int kKey;
 
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ, 
                            &kKey,
                            (void*)tag,
                            (dispatch_function_t)CFRelease);

dispatch_sync(workerQ, ^{
    // is funnelQ in the hierarchy of workerQ?
    CFStringRef tag = dispatch_get_specific(&kKey);
    if (tag){
        dispatch_sync(funnelQ, ^{
            // some task
        });
    } else {
        // some task
    }
});

解释:

  • 我创建了一个workerQ指向队列的funnelQ队列。在实际代码中,如果您有多个“工作”队列并且想要一次恢复/暂停所有队列(这通过恢复/更新它们的目标funnelQ队列来实现),这将很有用。
  • 我可能会在任何时间点汇集我的工人队列,所以要知道它们是否被汇集,我funnelQ用“漏斗”这个词标记。
  • 一路走来,我dispatch_sync想去workerQ,无论出于何种原因,但dispatch_syncfunnelQ避免将 dispatch_sync 发送到当前队列,所以我检查标签并采取相应措施。因为 get 沿着层次结构向上走,所以不会在 中找到该值,workerQ但会在funnelQ. 这是一种找出层次结构中是否有任何队列是我们存储值的队列的方法。因此,要防止 dispatch_sync 到当前队列。

如果您想知道读/写上下文数据的函数,有以下三个:

  • dispatch_queue_set_specific: 写入队列。
  • dispatch_queue_get_specific: 从队列中读取。
  • dispatch_get_specific:从当前队列中读取的便利功能。

键通过指针进行比较,并且从不取消引用。setter 中的最后一个参数是释放键的析构函数。

如果您想知道“将一个队列指向另一个队列”,那就是这个意思。例如,我可以将队列 A 指向主队列,它会导致队列 A 中的所有块都在主队列中运行(通常是为了 UI 更新而这样做)。

于 2013-03-31T02:50:54.313 回答
53

我在文档(最后一章)中找到了这个:

不要从在传递给函数调用的同一队列上执行的任务调用 dispatch_sync 函数。这样做会使队列死锁。如果您需要分派到当前队列,请使用 dispatch_async 函数异步执行此操作。

另外,我按照您提供的链接在dispatch_sync的描述中阅读了以下内容:

调用此函数并以当前队列为目标会导致死锁。

所以我认为这不是 GCD 的问题,我认为唯一明智的方法是您在发现问题后发明的方法。

于 2012-06-12T14:02:19.520 回答
16

我知道你的困惑来自哪里:

作为一种优化,此函数在可能的情况下调用当前线程上的块。

小心,上面写着current thread

线程!=队列

队列不拥有线程,并且线程未绑定到队列。有线程,有队列。每当一个队列想要运行一个块时,它都需要一个线程,但这并不总是同一个线程。它只需要它的任何线程(这可能每次都不同),当它完成运行块时(目前),同一个线程现在可以被不同的队列使用。

这句话讲的优化是关于线程的,不是关于队列的。例如,假设您有两个串行队列,QueueA现在QueueB您执行以下操作:

dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});

QueueA运行该块时,它将暂时拥有一个线程,任何线程。someFunctionA(...)将在该线程上执行。现在在进行同步调度时,QueueA不能做任何其他事情,它必须等待调度完成。QueueB另一方面,还需要一个线程来运行它的块并执行someFunctionB(...)。因此,要么QueueA暂时挂起其线程并QueueB使用其他线程来运行该块,要么QueueA将其线程移交给QueueB(毕竟在同步调度完成之前它无论如何都不需要它)并QueueB直接使用QueueA.

不用说最后一个选项要快得多,因为不需要线程切换。这就是句话所说的优化。所以一个dispatch_sync()不同的队列可能并不总是导致线程切换(不同的队列,可能是同一个线程)。

但是dispatch_sync()仍然不能发生在同一个队列上(同一个线程,是的,同一个队列,不是)。这是因为一个队列会一个接一个地执行块,当它当前执行一个块时,它不会执行另一个块,直到当前执行完成。所以它在同一个队列上执行BlockABlockA执行 a dispatch_sync()of 。只要BlockB队列还在运行,它就不会运行,但在运行之前不会继续运行。看到问题了吗?这是一个经典的僵局。BlockBBlockABlockABlockB

于 2015-07-04T00:06:47.323 回答
6

文档明确指出,传递当前队列会导致死锁。

现在他们没有说他们为什么要这样设计东西(除了它实际上需要额外的代码才能使它工作),但我怀疑这样做的原因是因为在这种特殊情况下,块会“跳跃”队列,即在正常情况下,您的块最终会在队列中的所有其他块都运行后运行,但在这种情况下它会在之前运行。

当您尝试将 GCD 用作互斥机制时会出现此问题,这种特殊情况相当于使用递归互斥锁。我不想讨论是使用 GCD 还是使用传统的互斥 API(例如 pthreads 互斥锁)更好,甚至使用递归互斥锁是否是个好主意。我会让其他人对此进行争论,但肯定有这种需求,尤其是当它是您正在处理的主队列时。

就个人而言,我认为如果 dispatch_sync 支持这一点或者如果有另一个函数提供了替代行为,它会更有用。我会敦促其他有此想法的人向 Apple 提交错误报告(正如我所做的那样,ID:12668073)。

您可以编写自己的函数来做同样的事情,但这有点小技巧:

// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
                                          dispatch_block_t block)
{
  dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
  if (dispatch_get_specific (queue))
    block ();
  else
    dispatch_sync (queue, block);
}

注意 以前,我有一个使用 dispatch_get_current_queue() 的示例,但现在已弃用。

于 2012-11-09T02:08:40.847 回答
4

两者都dispatch_async执行dispatch_sync将它们的操作推送到所需的队列中。该动作不会立即发生;它发生在队列运行循环的某些未来迭代中。dispatch_async和之间的区别在于dispatch_sync阻塞dispatch_sync当前队列直到动作完成。

想想当你在当前队列上异步执行某事时会发生什么。同样,它不会立即发生。它将它放在一个 FIFO 队列中,并且它必须等到运行循环的当前迭代完成之后(并且可能还等待队列中的其他操作,然后再执行此新操作)。

现在您可能会问,在对当前队列异步执行操作时,为什么不总是直接调用该函数,而不是等到未来某个时间。答案是两者之间有很大的不同。很多时候,你需要执行一个动作,但它需要在当前运行循环的迭代中由堆栈向上的函数执行任何副作用之后执行;或者您需要在运行循环中已经安排的一些动画操作等之后执行您的操作。这就是为什么很多时候您会看到代码[obj performSelector:selector withObject:foo afterDelay:0](是的,它不同于[obj performSelector:selector withObject:foo])。

正如我们之前所说,dispatch_sync与 相同dispatch_async,只是它会阻塞直到动作完成。所以很明显它为什么会死锁——至少在运行循环的当前迭代完成之后,该块才能执行;但我们正在等待它完成,然后再继续。

理论上,dispatch_sync当它是当前线程时,可以为 for 制定一个特殊情况,立即执行它。(对于 存在这种特殊情况performSelector:onThread:withObject:waitUntilDone:,当线程是当前线程并且waitUntilDone:为 YES 时,它会立即执行它。)但是,我猜 Apple 决定最好在这里保持一致的行为,而不管队列如何。

于 2012-06-11T19:59:43.503 回答
2

从以下文档中找到。 https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//apple_ref/c/func/dispatch_sync

dispatch_async不同,“ dispatch_sync ”函数在块完成之前不会返回。调用此函数并以当前队列为目标会导致死锁。

dispatch_async不同,目标队列不执行保留。因为对该函数的调用是同步的,所以它“借用”了调用者的引用。此外,不会对块执行Block_copy 。

作为一种优化,此函数在可能的情况下调用当前线程上的块。

于 2015-12-04T06:22:38.900 回答