我为什么陷入困境?
- (void)foo { static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ [self foo]; }); // whatever... }
我希望foo在第一次通话时执行两次.
解决方法
现有的答案都不是很准确(一个是错误的,另一个是有点误导,错过了一些关键细节).首先,让我们去
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信号量一次.在该walk的开始,onceToken再次被更改为DISPATCH_ONCE_DONE(它被方便地定义为永远不会是有效指针的值,因此永远不会是被阻塞的调用者的链接列表的头部.)将其更改为DISPATCH_ONCE_DONE使后续调用者(在进程的剩余生命周期内)检查已完成状态变得便宜.
所以在你的情况下,发生的事情是这样的:
>第一次调用-foo时,onceToken为nil(由于静态保证被初始化为0而得到保证),并且原子地改变为成为服务员链接列表的头部.
>当你从块内部递归调用-foo时,你的线程被认为是“第二个调用者”,并且存在于这个新的较低堆栈帧中的服务器结构被添加到列表中然后你去等待信号量.
>这里的问题是这个信号量永远不会发出信号,因为为了让它发出信号,你的块必须完成执行(在更高的堆栈帧中),现在由于死锁而不能发生.
所以,简而言之,是的,你已陷入僵局,这里的实际需要是“不要试图递归调用dispatch_once块”.但问题绝对不是“无限递归”,并且该标志绝对不仅仅在块完成执行后更改 – 在块执行之前更改它正是它如何知道使调用者2..N等待调用者1完成.