140 votes

Concurrent vs files séquentiels dans GCD

Je lutte pour comprendre pleinement les files d'attente concurrentes et sérielles dans GCD. J'ai quelques problèmes et j'espère que quelqu'un pourra répondre clairement et de manière concise.

  1. Je lis que les files d'attente sérielles sont créées et utilisées pour exécuter des tâches les unes après les autres. Cependant, que se passe-t-il si :

    • Je crée une file d'attente sérielle
    • J'utilise dispatch_async (sur la file d'attente sérielle que je viens de créer) trois fois pour expédier trois blocs A, B, C

    Est-ce que les trois blocs seront exécutés :

    • dans l'ordre A, B, C car la file d'attente est sérielle

      OU

    • en parallèle (en même temps sur des threads parallèles) car j'ai utilisé un envoi ASYNCHRONE

  2. Je lis que je peux utiliser dispatch_sync sur des files d'attente concurrentes pour exécuter des blocs les uns après les autres. Dans ce cas, POURQUOI les files d'attente sérielles existent-elles, puisque je peux toujours utiliser une file d'attente concurrente où je peux expédier SYNCHRONENT autant de blocs que je veux?

    Merci pour toute bonne explication!

247voto

Stephen Darlington Points 33587

Un exemple simple : vous avez un bloc qui prend une minute pour s'exécuter. Vous l'ajoutez à une file d'attente depuis le thread principal. Regardons les quatre cas.

  • async - concurrent : le code s'exécute sur un thread en arrière-plan. Le contrôle revient immédiatement au thread principal (et à l'IU). Le bloc ne peut pas supposer qu'il est le seul bloc s'exécutant dans cette file d'attente
  • async - séquentiel : le code s'exécute sur un thread en arrière-plan. Le contrôle revient immédiatement au thread principal. Le bloc peut supposer qu'il est le seul bloc s'exécutant dans cette file d'attente
  • sync - concurrent : le code s'exécute sur un thread en arrière-plan mais le thread principal attend sa fin, bloquant toute mise à jour de l'IU. Le bloc ne peut pas supposer qu'il est le seul bloc s'exécutant dans cette file d'attente (j'aurais pu ajouter un autre bloc en utilisant async quelques secondes plus tôt)
  • sync - séquentiel : le code s'exécute sur un thread en arrière-plan mais le thread principal attend sa fin, bloquant toute mise à jour de l'IU. Le bloc peut supposer qu'il est le seul bloc s'exécutant dans cette file d'attente

Évidemment, vous n'utiliseriez pas les deux derniers pour des processus longs. Vous les voyez normalement lorsque vous essayez de mettre à jour l'IU (toujours sur le thread principal) à partir de quelque chose qui peut s'exécuter sur un autre thread.

140voto

LC 웃 Points 15362

Voici quelques expériences que j'ai faites pour me comprendre sur ces files d'attente série, concurrent avec Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

   let serialQueue = DispatchQueue(label: "com.queue.Serial")
      for i in 1...5 {
        serialQueue.async {

            if Thread.isMainThread{
                print("tâche en cours sur le thread principal")
            }else{
                print("tâche en cours sur le thread arrière-plan")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) téléchargement complet")
        }
    }
}

La tâche sera exécutée dans un thread différent (autre que le thread principal) lorsque vous utilisez async dans GCD. Async signifie exécuter la ligne suivante sans attendre que le bloc s'exécute, ce qui entraîne un thread principal et une file d'attente principale non bloquants. Depuis qu'il s'agit d'une file d'attente sérielle, toutes les tâches sont exécutées dans l'ordre où elles sont ajoutées à la file d'attente sérielle. Les tâches exécutées de manière sérielle sont toujours exécutées une par une par le thread unique associé à la file d'attente.

func doLongSyncTaskInSerialQueue() {
    let serialQueue = DispatchQueue(label: "com.queue.Serial")
    for i in 1...5 {
        serialQueue.sync {
            if Thread.isMainThread{
                print("tâche en cours sur le thread principal")
            }else{
                print("tâche en cours sur le thread arrière-plan")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) téléchargement complet")
        }
    }
}

La tâche peut s'exécuter dans le thread principal lorsque vous utilisez sync dans GCD. Sync exécute un bloc sur une file d'attente donnée et attend sa complétion, ce qui entraîne le blocage du thread principal ou de la file d'attente principale. Comme la file d'attente principale doit attendre que le bloc traité soit terminé, le thread principal sera disponible pour traiter des blocs provenant de files d'attente autres que la file d'attente principale. Par conséquent, il y a une chance que le code exécuté sur la file d'attente arrière-plan puisse réellement s'exécuter sur le thread principal. Étant donné qu'il s'agit d'une file d'attente sérielle, toutes les tâches sont exécutées dans l'ordre où elles sont ajoutées (FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("tâche en cours sur le thread principal")
            }else{
                print("tâche en cours sur le thread arrière-plan")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) téléchargement complet")
        }
        print("\(i) en cours d'exécution")
    }
}

La tâche s'exécutera dans le thread arrière-plan lorsque vous utilisez async dans GCD. Async signifie exécuter la ligne suivante sans attendre que le bloc s'exécute, ce qui entraîne un thread principal non bloquant. Rappelez-vous dans une file d'attente concurrente, les tâches sont traitées dans l'ordre où elles sont ajoutées à la file d'attente mais avec des threads différents attachés à la file. Rappelez-vous qu'elles ne sont pas censées terminer la tâche dans l'ordre où elles sont ajoutées à la file. L'ordre des tâches diffère à chaque fois que des threads sont créés automatiquement si nécessaire. Les tâches sont exécutées en parallèle. Avec plus que cela (maxConcurrentOperationCount) est atteint, certaines tâches se comporteront comme une série jusqu'à ce qu'un thread soit libre.

func doLongSyncTaskInConcurrentQueue() {
  let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.sync {
            if Thread.isMainThread{
                print("tâche en cours sur le thread principal")
            }else{
                print("tâche en cours sur le thread arrière-plan")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) téléchargement complet")
        }
        print("\(i) exécuté")
    }
}

La tâche peut s'exécuter dans le thread principal lorsque vous utilisez sync dans GCD. Sync exécute un bloc sur une file d'attente donnée et attend sa complétion, ce qui entraîne le blocage du thread principal ou de la file d'attente principale. Comme la file d'attente principale doit attendre que le bloc traité soit terminé, le thread principal sera disponible pour traiter des blocs provenant de files d'attente autres que la file d'attente principale. Par conséquent, il y a une chance que le code exécuté sur la file d'attente arrière-plan puisse réellement s'exécuter sur le thread principal. Étant donné qu'il s'agit d'une file d'attente concurrente, les tâches peuvent ne pas être terminées dans l'ordre où elles sont ajoutées à la file. Mais avec une opération synchrone, c'est le cas, même si elles peuvent être traitées par différents threads. Ainsi, cela se comporte comme s'il s'agissait de la file d'attente sérielle.

Voici un résumé de ces expériences

Rappelez-vous qu'en utilisant GCD, vous ajoutez uniquement des tâches à la file et exécutez des tâches à partir de cette file. La file envoie vos tâches soit dans le thread principal soit dans un thread arrière-plan en fonction que l'opération soit synchrone ou asynchrone. Les types de files sont File sérielle, Concurrente, File d'attente principale. Toutes les tâches que vous effectuez se font par défaut à partir de la file d'attente principale. Il existe déjà quatre files d'attente concurrentes globales prédéfinies pour votre application à utiliser et une file d'attente principale (DispatchQueue.main). Vous pouvez également créer manuellement votre propre file et exécuter des tâches à partir de cette file.

Les tâches liées à l'interface utilisateur doivent toujours être effectuées à partir du thread principal en planifiant la tâche dans la file principale. L'abréviation est DispatchQueue.main.sync/async, alors que les opérations liées au réseau ou lourdes doivent toujours être effectuées de manière asynchrone, quelle que soit le thread que vous utilisez, que ce soit le thread principal ou un thread arrière-plan.

MODIFICATION : Cependant, il y a des cas où il est nécessaire d'exécuter des appels réseau de manière synchrone dans un thread arrière-plan sans figer l'interface utilisateur (par exemple, rafraîchir le jeton OAuth et attendre qu'il réussisse ou non). Vous devez envelopper cette méthode dans une opération asynchrone. De cette manière, vos opérations lourdes sont exécutées dans l'ordre et sans bloquer le thread principal.

func doMultipleSyncTaskWithinAsynchronousOperation() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    concurrentQueue.async {
        let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        for i in 1...5 {
            concurrentQueue.sync {
                let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
                let _ = try! Data(contentsOf: imgURL)
                print("\(i) téléchargement complet")
            }
            print("\(i) exécutée")
        }
    }
}

MODIFICATION MODIFICATION : Vous pouvez regarder la vidéo de démonstration ici

9voto

Yunus Nedim Mehel Points 2311

J'aime penser à cela en utilisant cette métaphore (Voici le lien vers l'image originale) :

Papa aura besoin d'aide

Imaginons que votre père est en train de faire la vaisselle et que vous venez de boire un verre de soda. Vous apportez le verre à votre père pour qu'il le nettoie, le mettant à côté de l'autre vaisselle.

Maintenant votre père fait la vaisselle tout seul, donc il va devoir les faire une par une : Votre père ici représente une file d'attente sérielle.

Mais vous n'êtes pas vraiment intéressé à rester là à regarder la vaisselle se nettoyer. Donc, vous laissez tomber le verre et retournez dans votre chambre : cela s'appelle une répartition asynchrone. Votre père pourrait ou non vous prévenir une fois qu'il a terminé, mais l'important est que vous n'attendez pas que le verre soit nettoyé; vous retournez dans votre chambre pour faire, vous savez, des trucs de gamin.

Imaginons maintenant que vous avez encore soif et que vous voulez boire de l'eau dans ce même verre qui se trouve être votre préféré, et que vous voulez vraiment le récupérer dès qu'il est nettoyé. Donc, vous restez là et regardez votre père faire la vaisselle jusqu'à ce que la vôtre soit finie. Il s'agit d'une répartition synchrone, car vous êtes bloqué pendant que vous attendez que la tâche soit terminée.

Et enfin, disons que votre mère décide d'aider votre père et le rejoigne pour faire la vaisselle. Maintenant la file d'attente devient une file d'attente concurrente puisqu'ils peuvent nettoyer plusieurs plats en même temps; mais notez que vous pouvez toujours décider d'attendre là ou de retourner dans votre chambre, peu importe comment ils travaillent.

J'espère que cela aide

7voto

Keith Points 66

Si je comprends correctement comment fonctionne le GCD, je pense qu'il existe deux types de DispatchQueue, sériel et concurrent, en même temps, il existe deux façons dont DispatchQueue répartit ses tâches, le closure attribué, la première est async, et l'autre est sync. Ensemble, ils déterminent comment la fermeture (tâche) est effectivement exécutée.

J'ai découvert que sériel et concurrent signifient combien de threads cette file d'attente peut utiliser, sériel signifie un, tandis que concurrent signifie plusieurs. Et sync et async signifient que la tâche sera exécutée sur quel thread, le thread de l'appelant ou le thread sous-jacent de cette file d'attente, sync signifie exécuter sur le thread de l'appelant, tandis que async signifie exécuter sur le thread sous-jacent.

Voici le code expérimental qui peut s'exécuter sur Xcode Playground.

PlaygroundPage.current.needsIndefiniteExecution = true
let cq = DispatchQueue(label: "concurrent.queue", attributes: .concurrent)
let cq2 = DispatchQueue(label: "concurent.queue2", attributes: .concurrent)
let sq = DispatchQueue(label: "serial.queue")

func codeFragment() {
  print("début du Fragment de code")
  print("Tâche Thread:\(Thread.current.description)")
  let imgURL = URL(string: "http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground")!
  let _ = try! Data(contentsOf: imgURL)
  print("Fragment de code terminé")
}

func serialQueueSync() { sq.sync { codeFragment() } }
func serialQueueAsync() { sq.async { codeFragment() } }
func concurrentQueueSync() { cq2.sync { codeFragment() } }
func concurrentQueueAsync() { cq2.async { codeFragment() } }

func tasksExecution() {
  (1...5).forEach { (_) in
    /// Utilisation d'une file d'attente concurrente pour simuler des exécutions de tâches concurrentes.
    cq.async {
      print("Thread de l'appelant:\(Thread.current.description)")
      /// File d'attente série asynchrone, les tâches s'exécutent séquentiellement, car il n'y a qu'un seul thread pouvant être utilisé par la file d'attente sérielle, le thread sous-jacent de la file d'attente sérielle.
      //serialQueueAsync()
      /// File d'attente sérielle synchrone, les tâches s'exécutent séquentiellement, car il n'y a qu'un seul thread pouvant être utilisé par la file d'attente sérielle, un par un des threads des appelants.
      //serialQueueSync()
      /// File d'attente concurrente asynchrone, les tâches s'exécutent de manière concurrente, car les tâches peuvent s'exécuter sur différents threads sous-jacents
      //concurrentQueueAsync()
      /// File d'attente concurrente synchrone, les tâches s'exécutent de manière concurrente, car les tâches peuvent s'exécuter sur différents threads des appelants
      //concurrentQueueSync()
    }
  }
}
tasksExecution()

J'espère que cela peut être utile.

3voto

CrazyPro007 Points 78

1. Je lis que les files séquentielles sont créées et utilisées pour exécuter des tâches les unes après les autres. Cependant, que se passe-t-il si:- • Je crée une file séquentielle • J'utilise dispatch_async (sur la file séquentielle que je viens de créer) trois fois pour envoyer trois blocs A, B, C

RÉPONSE:- Les trois blocs sont exécutés les uns après les autres. J'ai créé un code d'exemple pour aider à comprendre.

let serialQueue = DispatchQueue(label: "SampleSerialQueue")
//Bloc premier
serialQueue.async {
    for i in 1...10{
        print("Série - Première opération",i)
    }
}

//Bloc deuxième
serialQueue.async {
    for i in 1...10{
        print("Série - Deuxième opération",i)
    }
}
//Bloc troisième
serialQueue.async {
    for i in 1...10{
        print("Série - Troisième opération",i)
    }
}

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