2013-10-04 24 views
7

Tại sao tôi bế tắc?Tại sao tôi bị bế tắc với dispatch_once?

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

     [self foo]; 

    }); 

    // whatever... 
} 

Tôi mong đợi foo sẽ được thực hiện hai lần trong lần gọi đầu tiên.

+0

Có vẻ là một phương pháp đệ quy mà không cần bất kỳ điều kiện nghỉ ngơi, không làm điều đó ! – duDE

+1

tại sao u cần gọi foo hai lần? – manujmv

+0

TẠI SAO dod bạn muốn gọi nó đệ quy?!?!? – hfossli

Trả lời

22

Không có câu trả lời hiện có nào là khá chính xác (một câu trả lời sai, trường còn lại có chút sai lệch và bỏ sót một số chi tiết quan trọng). Đầu tiên, chúng ta hãy đi 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); 
    } 
} 

Vì vậy, những gì thực sự xảy ra là, trái với những câu trả lời khác, onceToken được thay đổi từ trạng thái ban đầu của nó NULL để trỏ đến một địa chỉ trên stack của người gọi đầu tiên &dow (gọi người gọi này 1). Điều này xảy ra trước khi chặn được gọi. Nếu có nhiều người gọi đến trước khi khối hoàn thành, họ sẽ được thêm vào danh sách người phục vụ được liên kết, người đứng đầu được chứa trong onceToken cho đến khi khối hoàn thành (gọi cho họ là người gọi 2..N). Sau khi được thêm vào danh sách này, người gọi 2..N đợi trên một semaphore cho người gọi 1 để hoàn thành thực hiện của khối, tại điểm người gọi 1 sẽ đi bộ danh sách liên kết báo hiệu semaphore một lần cho mỗi người gọi 2..N. Khi bắt đầu bước đi đó, onceToken được thay đổi lần nữa thành DISPATCH_ONCE_DONE (được xác định là giá trị không bao giờ là con trỏ hợp lệ và do đó không bao giờ là người đứng đầu danh sách người gọi bị chặn). nó để DISPATCH_ONCE_DONE là những gì làm cho nó rẻ cho người gọi tiếp theo (cho phần còn lại của đời của quá trình) để kiểm tra trạng thái hoàn thành.

Vì vậy, trong trường hợp của bạn, những gì đang xảy ra là thế này:

  • Lần đầu tiên bạn gọi -foo, onceToken là con số không (được bảo đảm bởi đức hạnh của tĩnh học được đảm bảo để được khởi tạo 0), và được nguyên tử thay đổi để trở thành người đứng đầu danh sách liên kết của người phục vụ.
  • Khi bạn gọi -foo đệ quy từ bên trong khối, chuỗi của bạn được coi là "người gọi thứ hai" và cấu trúc bồi bàn tồn tại trong khung ngăn xếp mới, thấp hơn này được thêm vào danh sách và sau đó bạn đợi trên semaphore.
  • Vấn đề ở đây là semaphore này sẽ không bao giờ được báo hiệu bởi vì để nó được báo hiệu, khối của bạn sẽ phải hoàn thành việc thực hiện (trong khung ngăn xếp cao hơn), mà bây giờ không thể xảy ra do bế tắc.

Vì vậy, trong ngắn hạn, có, bạn bế tắc và thực hành ở đây là "đừng cố gắng gọi đệ quy vào khối dispatch_once". Nhưng vấn đề là chắc chắn nhất KHÔNG "vô hạn đệ quy", và cờ là chắc chắn nhất không chỉthay đổi sau khi khối hoàn thành thi công - thay đổi nó trước khối thực thi là chính xác thế nào nó biết để làm cho người gọi 2.N đợi người gọi 1 kết thúc.

2

Bạn có thể thay đổi mã một chút, vì vậy mà các cuộc gọi nằm ngoài khối và không có bế tắc, một cái gì đó như thế này:

- (void)foo 
{ 
    static dispatch_once_t onceToken; 
    BOOL shouldRunTwice = NO; 
    dispatch_once(&onceToken, ^{ 
     shouldRunTwice = YES; 
    }); 
    if (shouldRunTwice) { 
     [self foo]; 
    } 
    // whatever... 
} 
Các vấn đề liên quan