4 votes

Est-ce que sync/async se comporte de la même manière que serial/concurrent, c'est-à-dire qu'ils contrôlent tous deux les DispatchQueues ou sync/Async contrôlent-ils uniquement les Threads ?

La plupart des réponses sur stackoverflow impliquent d'une certaine manière que le comportement sync vs async est assez similaire à la différence de concept de file d'attente série vs concurrente. Comme le lien dans le premier commentaire de @Roope.

J'ai commencé à penser que Série et concurrent sont liés à DispatchQueue et sync/async pour la façon dont une opération sera exécutée sur un thread. Est-ce que j'ai raison ?

Comme si on avait DQ.main.sync alors la fermeture de la tâche/opération sera exécutée de manière synchrone sur cette file d'attente série (principale). Et, si je fais DQ.main.async alors la tâche sera placée de manière asynchrone dans une autre file d'attente en arrière-plan, et une fois terminée, elle rendra le contrôle au thread principal. Et, puisque le thread principal est une file d'attente en série, il ne laissera aucune autre tâche/opération entrer en état d'exécution/ commencer à être exécutée avant que la tâche de fermeture actuelle n'ait terminé son exécution.

Ensuite, DQ.global().sync exécuterait une tâche de manière synchrone sur le thread auquel sa tâche/opération a été assignée, c'est-à-dire qu'il bloquerait ce thread d'effectuer toute autre tâche/opération en bloquant tout changement de contexte sur ce thread particulier. Et, puisque Global est une file d'attente concurrente, elle continuera à mettre les tâches qui y sont présentes en état d'exécution, quel que soit l'état d'exécution de la tâche/opération précédente.

DQ.global().async permettrait le changement de contexte sur le fil d'exécution sur lequel la fermeture de l'opération a été placée pour l'exécution.

Est-ce que c'est l'interprétation correcte des dispatchQueues ci-dessus et de sync vs async ?

3voto

Rob Points 70987

J'ai commencé à penser que Serial et concurrent sont liés à DispatchQueue, et sync/async pour la façon dont une opération sera exécutée sur un thread.

Oui, le choix d'une file d'attente série ou concurrente régit le comportement de la file d'attente vers laquelle vous effectuez la répartition, mais l'option sync / async n'a rien à voir avec la façon dont ce code s'exécute sur cette autre file. Au contraire, il dicte le comportement du thread à partir duquel vous l'avez envoyé. Donc, en bref :

  • Le fait que la file d'attente de destination soit sérielle ou concurrente détermine le comportement de cette file d'attente de destination (à savoir, si cette file d'attente peut exécuter cette fermeture en même temps que d'autres choses qui ont été distribuées à cette même file d'attente ou non) ;

  • Considérant que sync vs async dicte le comportement du thread actuel à partir duquel vous effectuez la répartition (à savoir, si le thread appelant doit attendre que le code réparti se termine ou non).

Ainsi, série/concurrent affecte la file d'attente de destination à que vous distribuez, alors que sync / async affecte le fil actuel de que vous envoyez.

Vous poursuivez :

Comme si on avait DQ.main.sync alors la fermeture de la tâche/opération sera exécutée de manière synchrone sur cette file d'attente série (principale).

Je pourrais reformuler ça en disant "si nous avons DQ.main.sync alors le thread actuel attendra que la file principale effectue cette fermeture".

N'oubliez pas que la "manière synchrone" n'a rien à voir avec ce qui se passe dans la file d'attente de destination (la file d'attente principale de votre système de gestion de l'information). DQ.main.sync exemple), mais plutôt le fil que vous avez appelé sync de. Le fil actuel va-t-il attendre ou non ?

FWIW, nous n'utilisons pas de DQ.main.sync très souvent, car 9 fois sur 10, nous le faisons juste pour envoyer une mise à jour de l'interface utilisateur, et il n'y a généralement pas besoin d'attendre. C'est mineur, mais nous utilisons presque toujours DQ.main.async . Nous utilisons sync c'est lorsque nous essayons de fournir une interaction sécurisée avec une ressource. Dans ce scénario, sync peut être très utile. Mais il n'est souvent pas nécessaire de l'associer à main mais ne fait qu'introduire des inefficacités.

Et, si je le fais DQ.main.async alors la tâche sera placée de manière asynchrone dans une autre file d'attente en arrière-plan, et une fois terminée, le contrôle sera rendu au thread principal.

Non.

Quand vous le faites DQ.main.async vous spécifiez que la fermeture sera exécutée de manière asynchrone sur la file d'attente principale (la file d'attente vers laquelle vous avez envoyé le message) et que votre thread actuel (vraisemblablement un thread d'arrière-plan) n'a pas besoin de l'attendre, mais continuera immédiatement.

Par exemple, considérons un exemple de demande de réseau, dont les réponses sont traitées sur une file d'attente série en arrière-plan de l'application URLSession :

let task = URLSession.shared.dataTask(with: url) { data, _, error in
    // parse the response
    DispatchQueue.main.async { 
        // update the UI
    }
    // do something else
}
task.resume()

Donc, l'analyse se fait sur ce URLSession Il envoie une mise à jour de l'interface utilisateur au thread principal, puis continue à faire autre chose sur ce thread d'arrière-plan. L'objectif principal de sync vs async est de savoir si le "faire autre chose" doit attendre que le "mettre à jour l'interface utilisateur" soit terminé ou non. Dans ce cas, il n'y a aucune raison de bloquer le thread d'arrière-plan actuel pendant que le thread principal traite la mise à jour de l'interface utilisateur. async .

Ensuite, DQ.global().sync exécuterait une tâche de manière synchrone sur le thread auquel sa tâche/opération a été assignée, c'est-à-dire ...

Oui DQ.global().sync dit "exécuter cette fermeture sur une file d'attente en arrière-plan, mais bloquer le thread actuel jusqu'à ce que cette fermeture soit terminée".

Inutile de dire qu'en pratique, nous ne ferions jamais DQ.global().sync . Il est inutile de bloquer le thread en cours en attendant que quelque chose s'exécute dans une file d'attente globale. L'intérêt de répartir les fermetures dans les files d'attente globales est de ne pas bloquer le thread en cours. Si vous envisagez de DQ.global().sync vous pouvez tout aussi bien l'exécuter sur le fil actuel, car vous le bloquez de toute façon. (En fait, GCD sait que DQ.global().sync n'apporte rien et, par souci d'optimisation, l'exécutera généralement sur le thread actuel de toute façon).

Maintenant, si tu devais utiliser async ou en utilisant une file d'attente personnalisée pour une raison quelconque, alors cela pourrait avoir du sens. Mais il n'y a généralement aucun intérêt à faire DQ.global().sync .

... cela empêchera ce thread d'effectuer toute autre tâche/opération en bloquant tout changement de contexte sur ce thread particulier.

Non.

Le site sync n'affecte pas "ce thread" (le thread de travail de la file d'attente globale). Le site sync affecte le actuel à partir duquel vous avez envoyé ce bloc de code. Ce thread actuel attendra-t-il la file d'attente globale pour exécuter le code distribué ( sync ) ou non ( async ) ?

Et, depuis, global est une file d'attente concurrente, elle continuera à faire passer les tâches qui y sont présentes à l'état d'exécution sans tenir compte de l'état d'exécution de la tâche/opération précédente.

Oui. Encore une fois, je pourrais reformuler ça : "Et, puisque global est une file d'attente en cours, cette fermeture sera programmée pour s'exécuter immédiatement, indépendamment de ce qui pourrait déjà être en cours d'exécution sur cette file d'attente".

La distinction technique est la suivante : lorsque vous envoyez quelque chose à une file d'attente concurrente, celle-ci démarre généralement immédiatement, mais pas toujours. Peut-être que tous les cœurs de votre CPU sont occupés à exécuter autre chose. Ou peut-être que vous avez envoyé de nombreux blocs et que vous avez temporairement épuisé le nombre très limité de "workers threads" de GCD. En résumé, bien qu'il démarre généralement immédiatement, il peut toujours y avoir des contraintes de ressources qui l'empêchent de le faire.

Mais c'est un détail : Conceptuellement, lorsque vous envoyez une file d'attente globale, oui, elle commence généralement à fonctionner immédiatement, même si vous avez peut-être quelques autres fermetures que vous avez envoyées à cette file d'attente et qui ne sont pas encore terminées.

DQ.global().async permettrait le changement de contexte sur le thread sur lequel la fermeture de l'opération a été placée pour exécution.

Je pourrais éviter l'expression "changement de contexte", car elle a une signification très spécifique qui dépasse probablement le cadre de cette question. Si vous êtes vraiment intéressés, vous pouvez voir la vidéo de la WWDC 2017. Modernisation de l'utilisation du dispatching de Grand Central .

La façon dont je décrirais DQ.global().async est qu'elle "permet simplement au thread actuel de continuer, non bloqué, pendant que la file d'attente globale effectue la fermeture distribuée". Il s'agit d'une technique extrêmement courante, souvent appelée depuis la file d'attente principale pour envoyer du code exigeant en termes de calcul vers une file d'attente globale, mais sans attendre qu'il se termine, laissant le thread principal libre de traiter les événements de l'interface utilisateur, ce qui se traduit par une interface utilisateur plus réactive.

0voto

Enricoza Points 857

Vous posez les bonnes questions mais je pense que vous êtes un peu confus (surtout à cause des messages pas très clairs sur ce sujet sur internet).

Concurrent / Série

Voyons comment créer une nouvelle file d'attente de distribution :

let serialQueue = DispatchQueue(label: label)

Si vous ne spécifiez aucun autre paramètre supplémentaire, cette file d'attente se comportera comme une file d'attente en série : Cela signifie que chaque bloc distribué sur cette file (sync ou async, peu importe) sera exécuté seul, sans possibilité pour d'autres blocs d'être exécutés, sur cette même file, simultanément.

Cela ne signifie pas que tout le reste est arrêté, cela signifie simplement que si quelque chose d'autre est distribué sur cette même file d'attente, il attendra que le premier bloc soit terminé avant de commencer son exécution. Les autres threads et files d'attente continueront à fonctionner de manière autonome.


Vous pouvez cependant créer une file d'attente concurrente, qui ne contraindra pas ces blocs de code de cette manière et, au lieu de cela, s'il arrive que plusieurs blocs de code soient distribués sur cette même file d'attente en même temps, elle les exécutera en même temps (sur différents threads).

let concurrentQueue = DispatchQueue(label: label,
                      qos: .background,
                      attributes: .concurrent,
                      autoreleaseFrequency: .inherit,
                      target: .global())

Ainsi, il suffit de passer l'attribut concurrent dans la file d'attente, et il ne sera plus en série.

(Je ne parlerai pas des autres paramètres car ils ne font pas l'objet de cette question particulière et, je pense, vous pouvez les lire dans l'autre billet de SO dont le lien figure dans le commentaire ou, si cela ne suffit pas, vous pouvez poser une autre question)


Si vous voulez en savoir plus sur les files d'attente concurrentes (aka : passez si vous ne vous souciez pas des files d'attente concurrentes)

On pourrait se demander : quand ai-je besoin d'une file d'attente concurrente ?

Par exemple, imaginons un cas d'utilisation où vous voulez synchroniser des lectures sur une ressource partagée : puisque les lectures peuvent être effectuées simultanément sans problème, vous pouvez utiliser une file d'attente concurrente pour cela.

Mais que faire si vous voulez écrire sur cette ressource partagée ? Eh bien, dans ce cas, une écriture doit agir comme une "barrière" et pendant l'exécution de cette écriture, aucune autre écriture et aucune lecture ne peuvent opérer sur cette ressource simultanément. Pour obtenir ce type de comportement, le code swift ressemblerait à ceci

concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })

En d'autres termes, vous pouvez faire en sorte qu'une file d'attente concurrente fonctionne temporairement comme une file d'attente en série en cas de besoin.


Une fois encore, la distinction simultanée/série n'est valable que pour les blocs distribués à cette même file d'attente, elle n'a rien à voir avec d'autres travaux simultanés ou en série qui peuvent être effectués sur un autre thread/une autre file d'attente.

SYNC / ASYNC

Il s'agit d'un tout autre problème, qui n'a pratiquement aucun lien avec le précédent.

Ces deux façons de distribuer un bloc de code sont relatives au fil d'exécution/à la file d'attente où vous vous trouvez au moment de l'appel de distribution. Cet appel de distribution bloque (en cas de synchronisation) ou ne bloque pas (asynchronisation) l'exécution de ce thread/queue pendant l'exécution du code que vous distribuez sur l'autre queue.

Disons que j'exécute une méthode et que dans cette méthode, je dispatche quelque chose d'asynchrone sur une autre file d'attente (j'utilise la file d'attente principale mais cela peut être n'importe quelle file d'attente) :

func someMethod() {
    var aString = "1"
    DispatchQueue.main.async {
        aString = "2"
    }
    print(aString)
}

Ce qui se passe, c'est que ce bloc de code est distribué sur une autre file d'attente et pourrait être exécuté en série ou simultanément sur cette file, mais cela n'a aucune corrélation avec ce qui se passe sur la file d'attente actuelle (qui est celle sur laquelle someMethod est appelé).

Ce qui se passe dans la file d'attente actuelle est que le code va continuer à s'exécuter et n'attendra pas que ce bloc soit terminé pour imprimer cette variable. Cela signifie que, très probablement, vous verrez l'impression de 1 et non de 2 (plus précisément, vous ne pouvez pas savoir ce qui se passera en premier).

Si, au contraire, vous l'aviez envoyé en synchro, vous auriez TOUJOURS imprimé 2 au lieu de 1, parce que la file d'attente actuelle aurait attendu que ce bloc de code soit terminé avant de poursuivre son exécution.

Donc, cela va imprimer 2 :

func someMethod() {
    var aString = "1"
    DispatchQueue.main.sync {
        aString = "2"
    }
    print(aString)
}

Mais cela signifie-t-il que la file d'attente sur laquelle une certaine méthode est appelée est réellement arrêtée ?

Eh bien, cela dépend de la file d'attente actuelle :

  • Si c'est une série, alors oui. Tous les blocs précédemment distribués dans cette file d'attente ou qui seront distribués dans cette file devront attendre que ce bloc soit terminé.
  • Si c'est un concurrent, alors non. Tous les blocs concurrents continueront leur exécution, seul ce bloc d'exécution spécifique sera bloqué, attendant que cet appel de distribution termine son travail. Bien sûr, si nous sommes dans le cas avec barrière, alors c'est comme pour les files d'attente en série.

Que se passe-t-il lorsque la file d'attente actuelle et la file d'attente sur laquelle nous répartissons sont les mêmes ?

En supposant que nous sommes sur des files d'attente en série (ce qui, je pense, sera la plupart de vos cas d'utilisation).

  • Dans le cas où nous expédions la synchronisation, que l'impasse. Rien ne sera plus jamais exécuté sur cette queue. C'est le pire qui puisse arriver.
  • Dans le cas d'une distribution asynchrone, le code sera exécuté à la fin de tout le code déjà distribué sur cette file d'attente (y compris, mais sans s'y limiter, le code qui s'exécute actuellement dans someMethod).

Soyez donc très prudent lorsque vous utilisez la méthode de synchronisation et assurez-vous que vous n'êtes pas sur la même file d'attente que celle dans laquelle vous êtes envoyé.

J'espère que cela vous a permis de mieux comprendre.

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