web-dev-qa-db-fra.com

Pourquoi ne pouvons-nous pas utiliser un dispatch_sync sur la file d'attente actuelle?

J'ai rencontré un scénario où j'avais un rappel de délégué qui pouvait se produire sur le thread principal ou sur un autre thread, et je ne le saurais pas avant l'exécution (en utilisant StoreKit.framework).

J'avais également du code d'interface utilisateur que je devais mettre à jour dans ce rappel qui devait se produire avant l'exécution de la fonction, donc ma pensée initiale était d'avoir une fonction comme celle-ci:

-(void) someDelegateCallback:(id) sender
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        // ui update code here
    });

    // code here that depends upon the UI getting updated
}

Cela fonctionne très bien, lorsqu'il est exécuté sur le thread d'arrière-plan. Cependant, lorsqu'il est exécuté sur le thread principal, le programme aboutit à un blocage.

Cela seul me semble intéressant, si je lis la documentation de dispatch_sync à droite, alors je m'attendrais à ce qu'il exécute simplement le bloc, sans se soucier de le planifier dans le runloop, comme dit ici :

En tant qu'optimisation, cette fonction appelle le bloc sur le thread actuel lorsque cela est possible.

Mais, ce n'est pas trop grave, cela signifie simplement un peu plus de frappe, ce qui m'amène à cette approche:

-(void) someDelegateCallBack:(id) sender
{
    dispatch_block_t onMain = ^{
        // update UI code here
    };

    if (dispatch_get_current_queue() == dispatch_get_main_queue())
       onMain();
    else
       dispatch_sync(dispatch_get_main_queue(), onMain);
}

Cependant, cela semble un peu en arrière. Était-ce un bug dans la création de GCD, ou y a-t-il quelque chose qui me manque dans la documentation?

57

J'ai trouvé cela dans la documentation (dernier chapitre) :

N'appelez pas la fonction dispatch_sync à partir d'une tâche qui s'exécute dans la même file d'attente que vous passez à votre appel de fonction. Cela bloquera la file d'attente. Si vous devez répartir dans la file d'attente actuelle, faites-le de manière asynchrone à l'aide de la fonction dispatch_async.

Aussi, j'ai suivi le lien que vous avez fourni et dans la description de dispatch_sync j'ai lu ceci:

L'appel de cette fonction et le ciblage de la file d'attente actuelle entraînent un blocage.

Donc, je ne pense pas que ce soit un problème avec GCD, je pense que la seule approche raisonnable est celle que vous avez inventée après avoir découvert le problème.

51
lawicko

dispatch_sync fait deux choses:

  1. mettre en file d'attente un bloc
  2. bloque le thread actuel jusqu'à ce que le blocage soit terminé

Étant donné que le thread principal est une file d'attente série (ce qui signifie qu'il n'utilise qu'un seul thread), l'instruction suivante:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});

entraînera les événements suivants:

  1. dispatch_sync place le bloc dans la file d'attente principale.
  2. dispatch_sync bloque le thread de la file d'attente principale jusqu'à la fin de l'exécution du bloc.
  3. dispatch_sync attend indéfiniment car le thread sur lequel le bloc est censé s'exécuter est bloqué.

La clé pour comprendre cela est que dispatch_sync n'exécute pas de blocs, il ne fait que les mettre en file d'attente. L'exécution aura lieu lors d'une prochaine itération de la boucle d'exécution.

L'approche suivante:

if (queueA == dispatch_get_current_queue()){
    block();
} else {
    dispatch_sync(queueA,block);
}

est parfaitement bien, mais sachez qu'il ne vous protégera pas des scénarios complexes impliquant une hiérarchie de files d'attente. Dans ce cas, la file d'attente actuelle peut être différente d'une file d'attente précédemment bloquée où vous essayez d'envoyer votre blocage. Exemple:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        // dispatch_get_current_queue() is B, but A is blocked, 
        // so a dispatch_sync(A,b) will deadlock.
        dispatch_sync(queueA, ^{
            // some task
        });
    });
});

Pour les cas complexes, lisez/écrivez les données de valeur-clé dans la file d'attente de répartition:

dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);

static int kKey;

// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ, 
                            &kKey,
                            (void*)tag,
                            (dispatch_function_t)CFRelease);

dispatch_sync(workerQ, ^{
    // is funnelQ in the hierarchy of workerQ?
    CFStringRef tag = dispatch_get_specific(&kKey);
    if (tag){
        dispatch_sync(funnelQ, ^{
            // some task
        });
    } else {
        // some task
    }
});

Explication:

  • Je crée une file d'attente workerQ qui pointe vers une file d'attente funnelQ. Dans le code réel, cela est utile si vous avez plusieurs files d'attente "de travail" et que vous souhaitez reprendre/suspendre toutes en même temps (ce qui est obtenu en reprenant/mettant à jour leur file d'attente cible funnelQ).
  • Je peux canaliser mes files d'attente de travail à tout moment, donc pour savoir si elles sont canalisées ou non, je tague funnelQ avec le mot "funnel".
  • Sur la route, je dispatch_sync quelque chose à workerQ, et pour une raison quelconque, je veux dispatch_sync à funnelQ, mais en évitant un dispatch_sync dans la file d'attente actuelle, je vérifie donc la balise et agis en conséquence. Parce que le get monte dans la hiérarchie, la valeur ne sera pas trouvée dans workerQ mais elle sera trouvée dans funnelQ. C'est une façon de savoir si une file d'attente dans la hiérarchie est celle où nous avons stocké la valeur. Et par conséquent, pour empêcher un dispatch_sync dans la file d'attente actuelle.

Si vous vous interrogez sur les fonctions qui lisent/écrivent des données de contexte, il y en a trois:

  • dispatch_queue_set_specific: Écrire dans une file d'attente.
  • dispatch_queue_get_specific: Lire à partir d'une file d'attente.
  • dispatch_get_specific: Fonction pratique pour lire dans la file d'attente actuelle.

La clé est comparée par pointeur et n'est jamais déréférencée. Le dernier paramètre du setter est un destructeur pour relâcher la clé.

Si vous vous demandez "pointant une file d'attente vers une autre", cela signifie exactement cela. Par exemple, je peux pointer une file d'attente A vers la file d'attente principale, et cela entraînera l'exécution de tous les blocs de la file d'attente A dans la file d'attente principale (cela se fait généralement pour les mises à jour de l'interface utilisateur).

69
Jano

Je sais d'où vient votre confusion:

En tant qu'optimisation, cette fonction appelle le bloc sur le thread actuel lorsque cela est possible.

Attention, il est dit thread actuel .

Thread! = File d'attente

Une file d'attente ne possède pas de thread et un thread n'est pas lié à une file d'attente. Il y a des threads et des files d'attente. Chaque fois qu'une file d'attente veut exécuter un bloc, elle a besoin d'un thread mais ce ne sera pas toujours le même thread. Il a juste besoin de n'importe quel thread (cela peut être différent à chaque fois) et quand il a fini d'exécuter des blocs (pour le moment), le même thread peut maintenant être utilisé par une file d'attente différente.

L'optimisation dont parle cette phrase concerne les threads, pas les files d'attente. Par exemple. considérez que vous avez deux files d'attente série, QueueA et QueueB et maintenant vous faites ce qui suit:

dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});

Lorsque QueueA exécute le bloc, il possède temporairement un thread, n'importe quel thread. someFunctionA(...) s'exécutera sur ce thread. Maintenant, lors de la répartition synchrone, QueueA ne peut rien faire d'autre, il doit attendre la fin de la répartition. QueueB d'autre part, aura également besoin d'un thread pour exécuter son bloc et exécuter someFunctionB(...). Donc, QueueA suspend temporairement son thread et QueueB utilise un autre thread pour exécuter le bloc ou QueueA remet son thread à QueueB (après tout, il a gagné je n'en ai pas besoin de toute façon jusqu'à ce que la répartition synchrone soit terminée) et QueueB utilise directement le thread actuel de QueueA.

Inutile de dire que la dernière option est beaucoup plus rapide car aucun changement de thread n'est requis. Et this est l'optimisation dont parle la phrase. Ainsi, une dispatch_sync() vers une file d'attente différente peut ne pas toujours provoquer un changement de thread (file d'attente différente, peut-être même thread).

Mais une dispatch_sync() ne peut toujours pas arriver à la même file d'attente (même thread, oui, même file d'attente, non). En effet, une file d'attente exécutera bloc après bloc et lorsqu'elle exécute actuellement un bloc, elle n'en exécutera pas un autre tant que l'exécution n'est pas terminée. Il exécute donc BlockA et BlockA fait une dispatch_sync() de BlockB dans la même file d'attente. La file d'attente ne s'exécutera pas BlockB tant qu'elle continuera d'exécuter BlockA, mais l'exécution de BlockA ne continuera pas tant que BlockB n'aura pas été exécuté. Vous voyez le problème? C'est une impasse classique.

14
Mecki

La documentation indique clairement que le dépassement de la file d'attente actuelle entraînera un blocage.

Maintenant, ils ne disent pas pourquoi ils ont conçu les choses de cette façon (sauf qu'il faudrait en fait du code supplémentaire pour le faire fonctionner), mais je soupçonne que la raison de faire les choses de cette façon est parce que dans ce cas spécial, les blocs "sautent" la file d'attente, c'est-à-dire que dans les cas normaux, votre bloc finit par s'exécuter après l'exécution de tous les autres blocs de la file d'attente, mais dans ce cas, il s'exécuterait avant.

Ce problème se produit lorsque vous essayez d'utiliser GCD comme mécanisme d'exclusion mutuelle, et ce cas particulier équivaut à utiliser un mutex récursif. Je ne veux pas entrer dans l'argument pour savoir s'il est préférable d'utiliser GCD ou une API d'exclusion mutuelle traditionnelle comme les mutex pthreads, ni même si c'est une bonne idée d'utiliser des mutex récursifs; Je laisserai les autres discuter de cela, mais il y a certainement une demande pour cela, en particulier lorsque c'est la file d'attente principale avec laquelle vous avez affaire.

Personnellement, je pense que dispatch_sync serait plus utile s'il supportait cela ou s'il y avait une autre fonction qui fournissait le comportement alternatif. J'exhorte les autres qui le pensent à déposer un rapport de bogue avec Apple (comme je l'ai fait, ID: 12668073).

Vous pouvez écrire votre propre fonction pour faire de même, mais c'est un peu un hack:

// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
                                          dispatch_block_t block)
{
  dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
  if (dispatch_get_specific (queue))
    block ();
  else
    dispatch_sync (queue, block);
}

N.B. Auparavant, j'avais un exemple qui utilisait dispatch_get_current_queue () mais qui est maintenant obsolète.

6
Chris Suter

Tous les deux dispatch_async et dispatch_sync effectuer Poussez leur action sur la file d'attente souhaitée. L'action ne se produit pas immédiatement; cela se produit lors d'une prochaine itération de la boucle d'exécution de la file d'attente. La différence entre dispatch_async et dispatch_sync est-ce dispatch_sync bloque la file d'attente actuelle jusqu'à la fin de l'action.

Pensez à ce qui se passe lorsque vous exécutez quelque chose de manière asynchrone dans la file d'attente actuelle. Encore une fois, cela ne se produit pas immédiatement; il le place dans une file d'attente FIFO, et il doit attendre la fin de l'itération actuelle de la boucle d'exécution (et peut-être aussi attendre d'autres actions qui étaient dans la file d'attente avant de mettre cela nouvelle action sur).

Vous pouvez maintenant vous demander, lorsque vous effectuez une action sur la file d'attente actuelle de manière asynchrone, pourquoi ne pas toujours simplement appeler la fonction directement, au lieu d'attendre un moment futur. La réponse est qu'il y a une grande différence entre les deux. Souvent, vous devez effectuer une action, mais elle doit être effectuée après quels que soient les effets secondaires qui sont effectués par les fonctions de la pile dans l'itération actuelle de la boucle d'exécution; ou vous devez effectuer votre action après une action d'animation déjà planifiée sur la boucle d'exécution, etc. C'est pourquoi, vous verrez souvent le code [obj performSelector:selector withObject:foo afterDelay:0] (oui, c'est différent de [obj performSelector:selector withObject:foo]).

Comme nous l'avons déjà dit, dispatch_sync est le même que dispatch_async, sauf qu'il se bloque jusqu'à la fin de l'action. Il est donc évident pourquoi il se bloquerait - le bloc ne peut s'exécuter qu'au moins une fois l'itération actuelle de la boucle d'exécution terminée; mais nous attendons qu'il se termine avant de continuer.

En théorie, il serait possible de faire un cas particulier pour dispatch_sync pour quand c'est le thread courant, pour l'exécuter immédiatement. (Un tel cas spécial existe pour performSelector:onThread:withObject:waitUntilDone:, lorsque le thread est le thread actuel et waitUntilDone: est OUI, il l'exécute immédiatement.) Cependant, je suppose Apple a décidé qu'il valait mieux avoir un comportement cohérent ici quelle que soit la file d'attente.

4
newacct

Trouvé dans la documentation suivante. https://developer.Apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//Apple_ref/c/func/dispatch_sync

Contrairement à dispatch_async , la fonction " dispatch_sync " ne revient que lorsque le bloc a fini. L'appel de cette fonction et le ciblage de la file d'attente actuelle entraînent un blocage.

Contrairement à dispatch_async , aucune conservation n'est effectuée sur la file d'attente cible. Parce que les appels à cette fonction sont synchrones, elle " emprunte " la référence de l'appelant. De plus, aucun Block_copy n'est effectué sur le bloc.

En tant qu'optimisation, cette fonction appelle le bloc sur le thread actuel lorsque cela est possible.

2
arango_86