2013-10-04 83 views
7

为什么我会死锁?为什么我会遇到dispatch_once死锁?

- (void)foo 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 

     [self foo]; 

    }); 

    // whatever... 
} 

我希望foo在首次调用时执行两次。

+0

似乎是没有任何破发状态的递归方法,不要做! – duDE

+1

为什么你需要调用foo两次? – manujmv

+0

为什么不想叫它递归呢?!?!? – hfossli

回答

22

现有的答案都不完全准确(一个是错误的,另一个有点误导性,并且忽略了一些关键细节)。首先,让我们去right to the source

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担保),并自动获取改为成为服务员链表的负责人。
  • 当您从块内部递归调用-foo时,您的线程被认为是“第二个调用者”,并且存在于此新的较低堆栈框架中的服务员结构被添加到列表中,然后您继续等待在信号量上。
  • 这里的问题是,这个信号量永远不会被发送,因为为了发出信号,你的块必须完成执行(在更高的堆栈帧中),现在由于死锁而不能执行。

所以,简而言之,是的,你已经陷入僵局,这里的实际需要是“不要试图递归调用dispatch_once块。但问题是,最肯定“无限循环”,并且标志是最绝不仅块后改变完成执行的 - 改变其之前的程序执行是正是它是如何知道使呼叫者2..N等待来电者1完成。

2

你可以修改代码一点,这样的调用是外块,而且也没有陷入僵局,这样的事情:

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