Pourquoi est-ce que je suis dans une impasse avec dispatch_once?

Pourquoi est-ce que je suis dans une impasse?

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

Je m'attends à ce que foo soit exécuté deux fois au premier appel.

Aucune des réponses existantes n'est assez précise (l'une est complètement fausse, l'autre est un peu trompeuse et manque certains détails critiques). D'abord, allons droit à la 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); } } 

Donc, ce qui se passe réellement est, contrairement aux autres réponses, le onceToken de l'état initial de NULL à une adresse sur la stack du premier appelant &dow (appelez cet appelant 1). Cela arrive avant que le bloc ne soit appelé. Si plus d'appelants arrivent avant la fin du bloc, ils sont ajoutés à une list chaînée de servers, dont la tête est contenue dans une fois onceToken jusqu'à ce que le bloc se termine (appelez les appelants 2..N). Après avoir été ajoutés à cette list, les appelants 2..N attendent sur un sémaphore que l'appelant 1 termine l'exécution du bloc, point auquel l'appelant 1 parcourra la list chaînée signalant le sémaphore une fois pour chaque appelant 2..N. Au début de cette marche, onceToken est modifié pour être DISPATCH_ONCE_DONE (ce qui est commodément défini pour être une valeur qui ne pourrait jamais être un pointeur valide, et ne pourrait donc jamais être la tête d'une list chaînée d'appelants bloqués). DISPATCH_ONCE_DONE est ce qui le rend peu coûteux pour les appelants suivants (pour le rest de la vie du process) pour vérifier l'état terminé.

Donc, dans votre cas, ce qui se passe est ceci:

  • La première fois que vous appelez -foo , onceToken est nul (ce qui est garanti par l'initialisation de la statique à 0), et devient atomiquement changé pour devenir la tête de la list chaînée des servers.
  • Lorsque vous appelez -foo récursivement depuis l'intérieur du bloc, votre thread est considéré comme un "second appelant" et une structure de server, qui existe dans ce nouveau cadre de stack inférieur, est ajoutée à la list, puis vous attendez le sémaphore.
  • Le problème ici est que ce sémaphore ne sera jamais signalé parce que pour qu'il soit signalé, votre bloc devrait terminer l'exécution (dans le cadre de la stack supérieure), ce qui ne peut plus se produire à cause d'un blocage.

Donc, en bref, oui, vous êtes dans l'impasse, et la pratique à retenir ici est, "n'essayez pas d'appeler récursivement dans un bloc de dispatch_once ." Mais le problème n'est certainement pas "récursion infinie", et le drapeau n'est certainement pas seulement changé après l'exécution du bloc – le changer avant que le bloc s'exécute est exactement comment il sait faire les appelants 2..N attendre l'appelant 1 pour finir.

Vous pourriez modifier un peu le code, de sorte que les appels sortent du bloc et qu'il n'y ait pas de blocage, quelque chose comme ceci:

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