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

时间:2012-06-11 17:25:08

标签: objective-c multithreading objective-c-blocks grand-central-dispatch

我遇到了一个场景,我有一个委托回调可能发生在主线程或另一个线程上,我不知道哪个直到运行时(使用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中,如{{3}所述{{3} }:

  

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

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

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

6 个答案:

答案 0 :(得分:69)

dispatch_sync做了两件事:

  1. 排队一个块
  2. 阻止当前线程,直到块完成运行
  3. 鉴于主线程是一个串行队列(这意味着它只使用一个线程),以下语句:

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

    将导致以下事件:

    1. dispatch_sync将主要队列中的块排队。
    2. dispatch_sync阻塞主队列的线程,直到块完成执行。
    3. dispatch_sync永远等待,因为块应该运行的线程被阻止。
    4. 理解这一点的关键是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_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)。

答案 1 :(得分:50)

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

  

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

另外,我按照您提供的链接和 dispatch_sync 的说明阅读了这篇文章:

  

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

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

答案 2 :(得分:14)

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

  

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

小心,它说当前线程

线程!=队列

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

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

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

QueueA运行块时,它将临时拥有一个线程,任何线程。 someFunctionA(...)将在该线程上执行。现在在进行同步调度时,QueueA无法执行任何其他操作,它必须等待调度完成。另一方面,QueueB还需要一个线程来运行它的块并执行someFunctionB(...)。因此,QueueA暂时挂起其线程,QueueB使用其他线程来运行阻止,或QueueA将其线程交给QueueB(毕竟它赢得了' t无论如何都需要它,直到同步调度完成)并且QueueB直接使用QueueA的当前线程。

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

但是dispatch_sync()仍然不能发生在同一个队列中(相同的线程,是的,相同的队列,没有)。这是因为队列将在块之后执行并且当它当前执行块时,它不会执行另一个队列,直到当前执行完成为止。因此它执行BlockABlockA在同一个队列中执行dispatch_sync() BlockB。只要代码仍然运行BlockB,该队列就不会运行BlockA,但运行BlockA会一直持续到BlockB运行。看到问题?这是一个经典的僵局。

答案 3 :(得分: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);
}

<子> N.B。以前,我有一个使用dispatch_get_current_queue()的示例,但现在已弃用。

答案 4 :(得分:4)

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决定在此处保持一致行为更好无论队列如何。

答案 5 :(得分:2)

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

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

dispatch_async 不同,不对目标队列执行保留。因为对此功能的调用是同步的,所以&#34; 借用&#34;来电者的参考。此外,在块上不执行 Block_copy

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