60 votes

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

J'ai couru dans un scénario où j'ai eu un délégué de rappel qui pourraient se produire sur le thread principal ou un autre thread, et je ne sais pas qui, jusqu'à l'exécution (à l'aide de StoreKit.framework).

J'ai aussi eu de l'INTERFACE utilisateur de code que j'avais besoin de mettre à jour dans ce rappel qui devait arriver avant la fonction exécutée, donc, ma première pensée était de disposer d'une fonction comme ceci:

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

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

Qui fonctionne très bien, lorsqu'il est exécuté sur le thread d'arrière-plan. Toutefois, lorsqu'il est exécuté sur le thread principal, le programme est livré à une impasse.

Cela seul me semble intéressant, si j'ai lu les docs pour dispatch_sync droit, alors je m'attends qu'il suffit d'exécuter le bloc d'emblée, de ne pas se soucier de la planification dans le runloop, comme dit ici:

Comme d'optimisation, cette fonction appelle le bloc sur le thread en cours lorsque cela est possible.

Mais, ce n'est pas trop grand d'une affaire, c'est tout simplement un peu plus de typage, qui m'amènent à 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);
}

Toutefois, cela semble un peu en arrière. Est-ce un bug dans la fabrication de PGCD, ou est-il quelque chose qui me manque dans les docs?

75voto

Jano Points 37593

dispatch_sync fait deux choses:

  1. la file d'attente d'un bloc
  2. bloque le thread courant jusqu'à ce que le bloc n'a pas fini de courir

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

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

va provoquer les événements suivants:

  1. dispatch_sync files d'attente du bloc dans la file d'attente principale.
  2. dispatch_sync bloque le thread de la file d'attente principale jusqu'à ce que le bloc a terminé l'exécution.
  3. dispatch_sync attend jamais, parce que le thread où le bloc est censé fonctionner est bloqué.

La clé de la compréhension de ce qu' dispatch_sync de non-exécution des blocs, il ne les met en attente. L'exécution aura lieu sur une future itération de l'exécution de la boucle.

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ège pas de scénarios complexes impliquant une hiérarchie des files d'attente. Dans de tels cas, le courant de la file d'attente peut être différent de celui précédemment bloqué file d'attente lorsque vous essayez d'envoyer votre bloc. 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, de lecture/écriture de données clé-valeur dans la file d'attente de dispatch:

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:

  • J'ai créer un workerQ de la file d'attente qui pointe vers un funnelQ de la file d'attente. Dans le code réel, c'est utile si vous avez plusieurs "travailleur" les files d'attente et que vous souhaitez reprendre, de suspendre tous à la fois (ce qui est réalisé par la reprise/mise à jour de leur cible funnelQ de la file d'attente).
  • J'ai peut-entonnoir mon travailleur files d'attente à n'importe quel point dans le temps, afin de savoir si ils sont canalisés ou pas, je balise funnelQ avec le mot "entonnoir".
  • En bas de la route j' dispatch_sync quelque chose d' workerQ, et pour quelque raison que ce soit, je veux dispatch_sync de funnelQ, mais en évitant un dispatch_sync à la file d'attente en cours, donc je check pour le tag et d'agir en conséquence. Parce que la obtenir des promenades de la hiérarchie, la valeur ne sera pas trouvé, en workerQ , mais il se trouve dans funnelQ. C'est une façon de découvrir si une file d'attente dans la hiérarchie est celui où nous avons stocké la valeur. Et donc, pour éviter un dispatch_sync à la file d'attente en cours.

Si vous vous interrogez sur les fonctions de lecture/écriture des données de contexte, il y 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: Le confort de la fonction de lecture à partir de la file d'attente en cours.

La clé est comparé par pointeur, et de ne jamais déréférencé. Le dernier paramètre dans le setter est un destructeur de libérer la clé.

Si vous vous demandez à propos de "pointant vers une file d'attente à l'autre", il veut dire exactement. Par exemple, je peux point de, une file d'attente à la file d'attente principale, et il sera la cause de tous les blocs dans la file d'attente à exécuter dans la file d'attente principale (généralement, cela se fait pour les mises à jour de l'INTERFACE utilisateur).

53voto

lawicko Points 3705

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

N'appelez pas le dispatch_sync fonction à partir d'une tâche en cours d'exécution sur la même file d'attente que vous transmettez à votre appel de fonction. Agir de la sorte impasse de la file d'attente. Si vous avez besoin de distribuer le courant de la file d'attente, ne de façon asynchrone à l'aide de la dispatch_async fonction.

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 en cours résultats dans l'impasse.

Donc je ne pense pas que c'est un problème avec PGCD, je pense que la seule approche raisonnable est celui que vous avez inventé après la découverte du problème.

6voto

Chris Suter Points 1938

La documentation indique clairement que l'adoption de la file d'attente en cours va provoquer un blocage.

Maintenant, ils ne disent pas pourquoi ils ont conçu les choses de cette façon (sauf qu'il serait effectivement prendre plus de code pour le faire fonctionner), mais je soupçonne que la raison pour faire les choses de cette façon parce que dans ce cas particulier, les blocs serait "sauter" la file d'attente, c'est à dire dans les cas normaux de votre bloc finit par courir après tous les autres blocs sur la file d'attente courir, mais dans ce cas, il irait à l'avant.

Ce problème se produit lorsque vous essayez d'utiliser le PGCD comme un mécanisme d'exclusion mutuelle, et ce cas particulier, est équivalent à l'utilisation récursive de mutex. Je ne veux pas entrer dans le débat de savoir s'il est préférable d'utiliser le PGCD ou un traditionnel exclusion mutuelle API telles que les pthreads mutex, ou même si c'est une bonne idée d'utiliser récursive mutex; je vais laisser les autres le font valoir à ce sujet, mais il y a certainement une demande pour ce, particulièrement lorsque c'est le principal de la file d'attente que vous avez affaire.

Personnellement, je pense que dispatch_sync serait plus utile si elle appuyait cette ou si il y a une autre fonction que si les nouvelles comportement. J'invite d'autres personnes qui pensent ainsi faire un rapport de bogue avec Apple (comme je l'ai fait, ID: 12668073).

Vous pouvez écrire votre propre fonction pour faire la même chose, 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 utilisé dispatch_get_current_queue (), mais qui est maintenant obsolète.

4voto

newacct Points 42530

Les deux dispatch_async et dispatch_sync effectuer pousser leur action sur la file d'attente voulue. L'action ne se fait pas immédiatement; il arrive sur certains des futurs itération de la course de la boucle de la file d'attente. La différence entre dispatch_async et dispatch_sync que dispatch_sync bloque la file d'attente en cours jusqu'à ce que l'action se termine.

Pensez à ce qui se passe lors de l'exécution de quelque chose de manière asynchrone sur la file d'attente en cours. Encore une fois, il ne se produit pas immédiatement; il le met dans une file d'attente FIFO, et il doit attendre la fin de l'itération courante de l'exécution de la boucle est fait (et peut-être attendre pour d'autres actions qui étaient dans la file d'attente avant de mettre cette nouvelle action).

Maintenant, vous pourriez demander, lors de l'exécution d'une action sur la file d'attente en cours de manière asynchrone, pourquoi ne pas toujours juste appeler la fonction directement, plutôt que d'attendre jusqu'à un certain moment dans l'avenir. La réponse est qu'il y a une grande différence entre les deux. Un grand nombre de fois, vous devez effectuer une action, mais il doit être effectué après ce que les effets secondaires sont effectuées par des fonctions de la pile dans l'itération en cours de l'exécution de la boucle; ou vous avez besoin pour effectuer votre action après l'animation de l'action qui est déjà prévu sur l'exécution de la boucle, etc. C'est pourquoi un grand nombre de fois, vous verrez le code [obj performSelector:selector withObject:foo afterDelay:0] (oui, c'est différent de [obj performSelector:selector withObject:foo]).

Comme nous l'avons dit avant, dispatch_sync est le même que dispatch_async, sauf qu'il bloque jusqu'à ce que l'action est terminée. Il est donc évident pourquoi il serait de blocage -- le bloc ne peut pas exécuter au moins jusqu'à après l'itération courante de l'exécution de la boucle est terminée, mais nous sommes à attendre la fin avant de continuer.

En théorie, il serait possible de faire un cas particulier pour dispatch_sync lorsque c'est le thread en cours, pour l'exécuter immédiatement. (Comme un cas particulier existe pour performSelector:onThread:withObject:waitUntilDone:, alors que le fil est le fil courant et waitUntilDone: est OUI, il l'exécute immédiatement.) Cependant, je pense qu'Apple a décidé qu'il était mieux d'avoir un comportement cohérent ici indépendamment de la file d'attente.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X