为啥我们不能在当前队列上使用 dispatch_sync?

Posted

技术标签:

【中文标题】为啥我们不能在当前队列上使用 dispatch_sync?【英文标题】:Why can't we use a dispatch_sync on the current queue?为什么我们不能在当前队列上使用 dispatch_sync? 【发布时间】:2012-06-14 15:09:01 【问题描述】:

我遇到了一个场景,我有一个委托回调,它可能发生在主线程或另一个线程上,直到运行时我才知道是哪个(使用 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 的文档,那么我希望它直接执行该块,而不用担心将其安排到运行循环中,如 here 所说:

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

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

-(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 制作中的错误,还是我在文档中遗漏了什么?

【问题讨论】:

dispatch_get_current_queue() 现在已弃用。检测主队列的方法是NSThread.isMainThread() (Swift) 或 [NSThread isMainThread] (Objective-C) NSThread.isMainThread() 不可靠,因为在极少数情况下主队列会阻塞,而 GCD 会重用主线程来执行其他队列。见1、2。 @jtbandes 将问题标记为重复时请小心。这个问题显然比您链接的问题更老,并且活动更多,也许它们应该在相反的方向关闭。 @RichardJ.RossIII:我确实考虑过;我欺骗它的 IMO 是一个更容易理解的问题,答案更全面。这个话题在meta.***.com/questions/315472/…讨论 【参考方案1】:

dispatch_sync 做了两件事:

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

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

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

将发生以下事件:

    dispatch_sync 将块放入主队列中。 dispatch_sync 阻塞主队列的线程,直到该块完成执行。 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
    
);

解释:

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

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

dispatch_queue_set_specific:写入队列。 dispatch_queue_get_specific:从队列中读取。 dispatch_get_specific:从当前队列中读取的便捷函数。

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

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

【讨论】:

显然这是正确的。 dispatch_sync 几乎永远不会走,我只需要它几次来更新和从我的应用程序的 UI 部分获取结果,除此之外,您需要选择其他东西。您检查队列层次结构的疯狂技术可能只会导致痛苦。 这很复杂,我宁愿有一个内置的amIChildOfQueue:,但使用特定于队列的上下文是 Apple 推荐用于复杂情况的解决方案。请参阅线程 dispatch_get_current_queue() deprecated 中的帖子 #6。 你能看看这个问题吗? ***.com/questions/19833744/… @Jano “阻塞当前队列直到块完成运行”是什么意思?人们说 dispatch_sync 只阻塞当前调用它的线程 @entropy 您引用的语句仅在队列只有一个线程时才成立,例如:主队列。我编辑了答案以澄清。【参考方案2】:

我在the documentation (last chapter)找到这个:

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

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

调用此函数并定位当前队列会导致死锁。

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

【讨论】:

我必须说我不同意 dispatch_sync 的行为方式有问题。如果您考虑一下,dispatch_syncasync 都会将任务排入队列,但第一个任务在任务执行之前也不会返回。在您提供的示例中,任务已排队但从未执行,这就是死锁的直接原因。所以请记住,这个函数的主要功能是实际排队任务,而不是调用它。调用是另一回事,但从您编写的内容来看,您似乎希望此函数实际调用您的任务。 我不同意。我对dispatch_sync 如何在幕后工作没有真正的兴趣,我关心的是,从上到下看,它的作用是execute this code on the given thread, and return when it's done。如果我在目标线程上,我不必检查我是否在目标线程上,因为该函数应该为我做这件事。这真的让我吃惊,虽然因为大多数苹果的 API 都比这更聪明,我猜开发人员只是在工作上偷懒了? :) @RichardJ.RossIII,您似乎忽略了您正在使用的 API 是一个串行队列这一事实,并且您在等待它后面的项目来执行。 API 没有做您希望它做的事情这一事实并不意味着它的实现很差。它完全按照文档记录的方式执行。 @Richard:我相信你的想法的错误在这里:«我关心的是,从上到下看,它所做的是在给定的 上执行此代码线程,并在它为 » 时返回。 dispatch_sync() 不适用于线程,它适用于队列。从dispatch_sync() 的角度来看,保证主队列在主线程上运行的事实是巧合。如果它立即执行您尝试加入队列的块,则会破坏其含义——在当前任务完成之前执行下一个任务意味着您将不再具有队列行为。 问题是,在 99.9% 的情况下,没有人真正想要真正的串行队列语义。他们不在乎秩序;他们只是不想并发。在某些情况下 dispatch_sync 语义是有意义的,但我认为它们导致问题的频率远远超过它们的帮助。也就是说,如果您只想在主线程上运行一些代码,performSelectorOnMainThread: 具有您正在寻找的语义。或者直接写 #define dispatch_sync_safe(queue, block) if (queue == dispatch_get_current_queue()) block(); else dispatch_sync(queue, block); 并调用它。【参考方案3】:

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

作为优化,此函数调用当前块 尽可能线程。

小心,上面写着当前线程

线程!=队列

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

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

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

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

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

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

【讨论】:

【参考方案4】:

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

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

当您尝试使用 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() 的示例,但现在已被弃用。

【讨论】:

我做了类似的事情,除了宏,所以我写的其他使用dispatch_sync的代码没有被破坏。 +1 给你! 宏也能很好地工作,但一般来说,我建议您只在不能使用静态内联函数时才使用宏,因为出于多种原因它们更可取,而宏没有任何优势. dispatch_get_current_queue 自 ios 6.x 起已弃用 这并不能防止死锁,因为您可以有一个目标队列为queue 的队列。然后你会进入else 分支然后死锁。 Apple 也记录了这一点:It is equally unsafe for code to assume that synchronous execution onto a queue is safe from deadlock if that queue is not the one returned by dispatch_get_current_queue(). from man 3 dispatch_get_current_queue under CAVEATS 对于主队列,您可以使用if ([NSThread isMainThread]) block() else dispatch_sync(dispatch_get_main_queue(), block); ,这是安全的,因为所有以主队列为目标的队列也在主线程上执行(因为主队列是串行队列)。 【参考方案5】:

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

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

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

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

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

【讨论】:

但这没有任何意义。至少应该有一条日志消息输出到控制台以防出错,就像其他 API 一样(例如递归 NSLock ing)。 @newacct "dispatch_sync 阻塞当前线程"?阻塞当前线程还是当前队列?【参考方案6】:

从以下文档中找到。 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

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

【讨论】:

以上是关于为啥我们不能在当前队列上使用 dispatch_sync?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不能在 ngFor 中使用 var 而不是 let

linux消息队列为啥不能删除

为啥我们不能在堆栈上分配动态内存?

为啥我们要“使用 2 个堆栈实现一个队列”? [复制]

电脑为啥不能用键盘Ctrl+VCtrl+C复制粘贴了

为啥 Java Collections 不能直接存储 Primitives 类型?