为什么我们不能在当前队列上使用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
做两件事情:
- 排队一个块
- 阻塞当前线程,直到块完成运行
鉴于主线程是一个串行队列(这意味着它只使用一个线程),下面的语句:
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 } });
说明:
- 我创build了一个
workerQ
队列的funnelQ
队列。 在真实代码中,如果您有多个“worker”队列,并且想要一次恢复/暂停(通过恢复/更新其目标funnelQ
队列来实现),这将非常有用。 - 我可能会在任何时候把我的工作人员排队,所以要知道他们是否漏斗,我用漏斗标记漏斗。
- 在路上我
dispatch_sync
东西workerQ
,无论出于什么原因,我想dispatch_sync
funnelQ
,但避免dispatch_sync到当前队列,所以我检查标签,并采取相应的行动。 因为get走向层次结构,所以在workerQ
找不到这个值,但是会在funnelQ
findfunnelQ
。 这是查找层次结构中是否有任何队列是我们存储值的方法。 因此,阻止dispatch_sync到当前队列。
如果您想知道读取/写入上下文数据的function,有三个:
-
dispatch_queue_set_specific
:写入队列。 -
dispatch_queue_get_specific
:从队列中读取。 -
dispatch_get_specific
:从当前队列中读取的便捷函数。
关键是通过指针进行比较,从不解除引用。 setter中的最后一个参数是释放键的析构函数。
如果你想知道“将一个队列指向另一个队列”,那就意味着这一点。 例如,我可以将队列A指向主队列,并且会导致队列A中的所有块在主队列中运行(通常这是为UI更新完成的)。
我知道你的困惑来自哪里:
作为优化,该函数尽可能地调用当前线程上的块。
小心,它说当前线程 。
线程!=队列
一个队列不拥有一个线程,一个线程没有绑定到一个队列。 有线程,有队列。 每当一个队列想要运行一个块时,它需要一个线程,但是这并不总是相同的线程。 它只是需要任何线程(这可能是一个不同的每次),当它完成运行块(目前),同一个线程现在可以由不同的队列使用。
这句话谈到的优化是关于线程,而不是关于队列。 例如考虑你有两个串行队列, 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()
仍然不能发生在同一个队列(同一个线程,是的,相同的队列,没有)。 这是因为一个队列将在块之后执行块,当它执行一个块时,它将不会执行另一个块直到完成。 所以它执行BlockA
,而BlockB
在相同的队列上执行BlockA
的dispatch_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_async
和dispatch_sync
将其操作推送到所需的队列中。 行动不会立即发生; 它发生在队列运行循环的将来迭代中。 dispatch_async
和dispatch_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队列。
与dispatch_async不同,在块完成之前,“ dispatch_sync ”函数不会返回。 调用此函数并将当前队列定位将导致死锁。
与dispatch_async不同,在目标队列上不执行任何保留。 因为对这个函数的调用是同步的,所以它“ 借用 ”调用者的引用。 而且,块上不执行Block_copy 。
作为优化,该函数尽可能地调用当前线程上的块。