¿Por qué estoy recibiendo un punto muerto con dispatch_once?

¿Por qué estoy estancado?

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

Espero que foo sea ​​ejecutado dos veces en la primera llamada.

Ninguna de las respuestas existentes es bastante precisa (una está completamente equivocada, la otra es un poco engañosa y pierde algunos detalles críticos). Primero, vayamos directo a la fuente :

 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); } } 

Entonces, lo que realmente sucede es que, al contrario de las otras respuestas, el onceToken se cambia de su estado inicial de NULL para apuntar a una dirección en la stack de la primera persona que llama &dow (llama a esta persona que llama). Esto sucede antes de llamar al bloque. Si más personas que llaman llegan antes de que se complete el bloque, se agregan a una list vinculada de camareros, cuyo jefe está contenido en una vez onceToken hasta que el bloque finaliza (llámelos llamadores 2..N). Después de ser agregado a esta list, los llamantes 2..N esperan en un semáforo para que el llamante 1 complete la ejecución del bloque, en ese punto el llamante 1 recorrerá la list enlazada indicando el semáforo una vez para cada llamante 2..N. Al comienzo de esa caminata, una vez que onceToken se cambia nuevamente para ser DISPATCH_ONCE_DONE (que se define convenientemente como un valor que nunca podría ser un puntero válido y, por lo tanto, nunca podría ser el jefe de una list enlazada de personas que llaman bloqueadas). Cambiarlo a DISPATCH_ONCE_DONE es lo que hace que sea barato para quienes llaman a continuación (durante el rest del process) verificar el estado completo.

Entonces, en tu caso, lo que está sucediendo es esto:

  • La primera vez que llama a -foo , onceToken es nil (que está garantizado en virtud de que estática garantiza que se inicializa a 0), y se cambia atómicamente para convertirse en el jefe de la list vinculada de meseros.
  • Cuando -foo recursivamente desde el interior del bloque, su hilo se considera "un segundo llamante" y se agrega a la list una estructura de mesero, que existe en este nuevo marco de stack inferior, y luego se va a esperar en el semáforo.
  • El problema aquí es que este semáforo nunca se señalizará porque para que se señalice, su bloque tendría que terminar de ejecutarse (en el marco de stack superior), lo que ahora no puede suceder debido a un punto muerto.

Entonces, en resumen, sí, estás en un punto muerto, y la guía práctica aquí es "no intentes llamar de forma recursiva a un bloque de dispatch_once ". Pero el problema definitivamente no es "recursión infinita", y la bandera definitivamente no solo cambia después de que el bloque completa la ejecución: cambiarlo antes de que el bloque se ejecute es exactamente como sabe hacer que las personas que llaman 2..N esperar a la persona que llama 1 para terminar.

Puede alterar un poco el código para que las llamadas se encuentren fuera del bloque y no haya interlocking, algo así:

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