为什么我会遇到dispatch_once死锁?

我为什么僵持?

- (void)foo { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self foo]; }); // whatever... } 

我希望foo在第一次通话时被执行两次。

现有的答案都不是十分准确的(一个是错误的,另一个是有点误导性的,并且忽略了一些关键细节)。 首先,让我们直接来源 :

 void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) { struct _dispatch_once_waiter_s * volatile *vval = (struct _dispatch_once_waiter_s**)val; struct _dispatch_once_waiter_s dow = { NULL, 0 }; struct _dispatch_once_waiter_s *tail, *tmp; _dispatch_thread_semaphore_t sema; if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) { dispatch_atomic_acquire_barrier(); _dispatch_client_callout(ctxt, func); dispatch_atomic_maximally_synchronizing_barrier(); //dispatch_atomic_release_barrier(); // assumed contained in above tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE); tail = &dow; while (tail != tmp) { while (!tmp->dow_next) { _dispatch_hardware_pause(); } sema = tmp->dow_sema; tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next; _dispatch_thread_semaphore_signal(sema); } } else { dow.dow_sema = _dispatch_get_thread_semaphore(); for (;;) { tmp = *vval; if (tmp == DISPATCH_ONCE_DONE) { break; } dispatch_atomic_store_barrier(); if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) { dow.dow_next = tmp; _dispatch_thread_semaphore_wait(dow.dow_sema); } } _dispatch_put_thread_semaphore(dow.dow_sema); } } 

那么真正发生的是,与其他答案相反, onceToken从其初始状态NULL改变为指向第一个调用者&dow (称为该调用者1)的堆栈上的地址。 在块被调用之前发生这种情况。 如果有更多的呼叫者在该街区完成之前到达,他们将被添加到一个服务员链接列表中,其头部包含在onceToken直到该街区完成(称他们为2..N)。 在被添加到该列表之后,呼叫者2 … N等待呼叫者1的信号完成该块的执行,此时呼叫者1将行走链接列表,向每个呼叫者2 … N发信号通知信号量一次。 在行走开始时, onceToken 再次变为DISPATCH_ONCE_DONE (这被方便地定义为永远不可能是有效指针的值,因此永远不可能是被阻塞呼叫者的链表的头部)。将其改变为DISPATCH_ONCE_DONE是后续调用者(进程的剩余时间)检查完成状态的便宜因素。

所以在你的情况下,发生了什么事情是这样的:

  • 第一次调用-fooonceToken是零(这是保证静态保证被初始化为0),并被primefaces地改变成为服务员的链表头。
  • 当你从块内部recursion地调用-foo ,你的线程被认为是“第二个调用者”,并且存在于这个新的较低的栈帧中的服务器结构被添加到列表中,然后你去等待信号。
  • 这里的问题是,这个信号量永远不会被发出信号,因为要发出信号,你的块将不得不完成执行(在更高的堆栈帧中),现在由于死锁而不能执行。

所以,简而言之,是的,你已经陷入僵局,这里的实际使用是“不要试图recursion调用dispatch_once块”。 但是这个问题绝对不是 “无限recursion”,并且在块完成执行之后,标志肯定不仅被改变 – 在块执行之前改变标志正是如何知道使得调用者2..N等待调用者1完成。

你可以稍微改变一下代码,这样调用就不在这个块之内了,而且没有死锁,就像这样:

 - (void)foo { static dispatch_once_t onceToken; BOOL shouldRunTwice = NO; dispatch_once(&onceToken, ^{ shouldRunTwice = YES; }); if (shouldRunTwice) { [self foo]; } // whatever... }