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

我遇到了一个场景,我有一个委托callback可能发生在主线程或另一个线程,我不知道哪些,直到运行时(使用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文档,那么我会期望它完全执行该块,而不必担心将其安排到runloop中,如下所述:

作为优化,该函数尽可能地调用当前线程上的块。

但是,这并不是什么大不了的事情,它只是意味着更多的打字,这使我有了这种方法:

 -(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制造中的一个错误,还是我在文档中缺less的东西?

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

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

另外,我跟着你提供的链接,在dispatch_sync的描述中我读到:

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

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

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 } }); 

说明:

  • 我创build了一个workerQ队列的funnelQ队列。 在真实代码中,如果您有多个“worker”队列,并且想要一次恢复/暂停(通过恢复/更新其目标funnelQ队列来实现),这将非常有用。
  • 我可能会在任何时候把我的工作人员排队,所以要知道他们是否漏斗,我用漏斗标记漏斗。
  • 在路上我dispatch_sync东西workerQ ,无论出于什么原因,我想dispatch_sync funnelQ ,但避免dispatch_sync到当前队列,所以我检查标签,并采取相应的行动。 因为get走向层次结构,所以在workerQ找不到这个值,但是会在funnelQfindfunnelQ 。 这是查找层次结构中是否有任何队列是我们存储值的方法。 因此,阻止dispatch_sync到当前队列。

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

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

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

如果你想知道“将一个队列指向另一个队列”,那就意味着这一点。 例如,我可以将队列A指向主队列,并且会导致队列A中的所有块在主队列中运行(通常这是为UI更新完成的)。

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

作为优化,该函数尽可能地调用当前线程上的块。

小心,它说当前线程

线程!=队列

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

这句话谈到的优化是关于线程,而不是关于队列。 例如考虑你有两个串行队列, QueueAQueueB ,现在你做了以下操作:

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

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

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

但是一个dispatch_sync()仍然不能发生在同一个队列(同一个线程,是的,相同的队列,没有)。 这是因为一个队列将在块之后执行块,当它执行一个块时,它将不会执行另一个块直到完成。 所以它执行BlockA ,而BlockB在相同的队列上执行BlockAdispatch_sync() 。 只要它仍然运行BlockA ,队列就不会运行BlockB ,但运行BlockA将不会继续,直到BlockB运行。 看到问题?

该文件明确指出,通过当前队列将导致死锁。

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

当您尝试使用GCD作为互斥机制时,会出现此问题,这种情况相当于使用recursion互斥。 我不想讨论是使用GCD还是传统的互斥API(比如pthreads互斥体),或者使用recursion互斥体是个好主意。 我会让别人为此争论,但肯定有这样的需求,特别是当它是你正在处理的主要队列。

我个人认为,如果dispatch_sync支持这一点,或者有其他函数提供了替代行为,dispatch_sync会更有用。 我会敦促那些认为如此的人向苹果公司提交错误报告(正如我所做的那样,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); } 

NB以前,我有一个使用dispatch_get_current_queue()的例子,但现在已经被弃用了。

dispatch_asyncdispatch_sync将其操作推送到所需的队列中。 行动不会立即发生; 它发生在队列运行循环的将来迭代中。 dispatch_asyncdispatch_sync之间的区别在于dispatch_async阻止当前队列,直到动作完成。

考虑一下当你在当前队列上asynchronous执行某些事情时会发生什么。 再次,它不会立即发生; 它将它放入一个FIFO队列中,并且必须等到运行循环的当前迭代完成之后(并且可能还要等待在执行此新操作之前在队列中的其他操作)。

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

正如我们之前所说的, dispatch_sync是一样的,不同的是它会阻塞直到动作完成。 所以很明显为什么它会死锁 – 至less在运行循环的当前迭代完成之后,块才能执行; 但我们正在等待它完成之前继续。

理论上讲,当它是当前线程时,可以为dispatch_sync做一个特殊情况,立即执行它。 ( performSelector:onThread:withObject:waitUntilDone:存在这种特殊情况,当线程是当前线程, waitUntilDone:是YES时,它立即执行。)但是,我猜苹果决定在这里有一致的行为不pipe队列。

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

作为优化,该函数尽可能地调用当前线程上的块。