2013-10-04 389 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... 
}