Files d'attente simultanées et séries dans GCD

117

J'ai du mal à comprendre pleinement les files d'attente simultanées et série dans GCD. J'ai quelques problèmes et j'espère que quelqu'un pourra me répondre clairement et précisément.

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

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

    Les trois blocs seront-ils exécutés:

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

      OU

    • simultanément (dans le même temps sur les threads parallèles) car j'ai utilisé la distribution ASYNC
  2. Je lis que je peux utiliser dispatch_syncsur des files d'attente simultanées afin d'exécuter les blocs les uns après les autres. Dans ce cas, POURQUOI existe-t-il même des files d'attente série, puisque je peux toujours utiliser une file d'attente simultanée où je peux distribuer de manière SYNCHRONE autant de blocs que je veux?

    Merci pour toute bonne explication!

Bogdan Alexandru
la source
Une simple bonne question prérequise répartit la synchronisation vs async
Honey

Réponses:

216

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

  • async - concurrent: le code s'exécute sur un thread d'arrière-plan. Le contrôle revient immédiatement au thread principal (et à l'interface utilisateur). Le bloc ne peut pas supposer que c'est le seul bloc en cours d'exécution sur cette file d'attente
  • async - serial: le code s'exécute sur un thread d'arrière-plan. Le contrôle revient immédiatement au thread principal. Le bloc peut supposer que c'est le seul bloc en cours d'exécution sur cette file d'attente
  • sync - concurrent: le code s'exécute sur un thread d'arrière-plan mais le thread principal attend qu'il se termine, bloquant toutes les mises à jour de l'interface utilisateur. Le bloc ne peut pas supposer que c'est le seul bloc en cours d'exécution sur cette file d'attente (j'aurais pu ajouter un autre bloc en utilisant async quelques secondes auparavant)
  • sync - serial: le code s'exécute sur un thread d'arrière-plan mais le thread principal attend qu'il se termine, bloquant toutes les mises à jour de l'interface utilisateur. Le bloc peut supposer que c'est le seul bloc en cours d'exécution sur cette file d'attente

De toute évidence, vous n'utiliserez aucun des deux derniers pour les processus de longue durée. Vous le voyez normalement lorsque vous essayez de mettre à jour l'interface utilisateur (toujours sur le thread principal) à partir de quelque chose qui peut être exécuté sur un autre thread.

Stephen Darlington
la source
14
Vous me dites donc que: (1) le type de la file d'attente (conc ou serial) est le SEUL élément qui décide si les tâches sont exécutées dans l'ordre ou en parallèle ;; (2) le type de répartition (sync ou async) indique seulement si l'exécution se déroule OU ne passe pas à l'instruction suivante? Je veux dire, si j'expédie une tâche SYNC, le code se bloquera jusqu'à ce que les tâches se terminent, quelle que soit la file d'attente sur laquelle il est exécuté?
Bogdan Alexandru
13
@BogdanAlexandru Correct. La file d'attente dicte la politique d'exécution, pas la manière dont vous mettez le bloc en file d'attente. La synchronisation attend que le bloc se termine, pas async.
Jano
2
@swiftBUTCHER Jusqu'à un certain point, oui. Lorsque vous créez une file d'attente, vous pouvez spécifier le nombre maximum de threads. Si vous ajoutez moins de tâches que cela, elles s'exécuteront en parallèle. Avec plus que cela, certaines tâches resteront dans une file d'attente jusqu'à ce qu'il y ait de la capacité disponible.
Stephen Darlington
2
@PabloA., Le thread principal est une file d'attente série donc il n'y a vraiment que deux cas. Au-delà, c'est exactement la même chose. Async retourne immédiatement (et le bloc est probablement exécuté à la fin de la boucle d'exécution actuelle). Le problème principal est si vous synchronisez du thread principal vers le thread principal, auquel cas vous obtenez un blocage.
Stephen Darlington
1
@ShauketSheikh Non. Le thread principal est une file d'attente série, mais toutes les files d'attente série ne sont pas le thread principal. Dans le quatrième point, le thread principal se bloquerait, attendant qu'un autre thread fasse concurrence à son travail. Si la file d'attente série était le thread principal, vous obtiendrez un blocage.
Stephen Darlington
122

Voici quelques expériences que je l' ai fait pour me faire comprendre au sujet de ces serial, les concurrentfiles d' attente avec Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

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

            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            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) completed downloading")
        }
    }
}

La tâche s'exécutera dans un thread différent (autre que le thread principal) lorsque vous utilisez async dans GCD. Async signifie exécuter la ligne suivante, n'attendez pas que le bloc s'exécute, ce qui entraîne le thread principal et la file d'attente principale non bloquants. Depuis sa file d'attente série, toutes sont exécutées dans l'ordre dans lequel elles sont ajoutées à la file d'attente série. Les tâches exécutées en série 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("task running in main thread")
            }else{
                print("task running in background thread")
            }
            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) completed downloading")
        }
    }
}

La tâche peut s'exécuter dans le thread principal lorsque vous utilisez la synchronisation dans GCD. Sync exécute un bloc sur une file d'attente donnée et attend qu'il se termine, ce qui entraîne le blocage du thread principal ou de la file d'attente principale. Puisque la file d'attente principale doit attendre la fin du bloc distribué, le thread principal sera disponible pour traiter les blocs des files d'attente autres que le Il y a donc une chance que le code s'exécutant sur la file d'attente d'arrière-plan s'exécute réellement sur le thread principal Depuis sa file d'attente série, tous sont exécutés dans l'ordre dans lequel ils sont ajoutés (FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            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) completed downloading")
        }
        print("\(i) executing")
    }
}

La tâche s'exécutera dans le thread d'arrière-plan lorsque vous utilisez async dans GCD. Async signifie exécuter la ligne suivante, n'attendez pas que le bloc s'exécute, ce qui entraîne le thread principal non bloquant. N'oubliez pas que dans la file d'attente simultanée, les tâches sont traitées dans l'ordre où elles sont ajoutées à la file d'attente, mais avec différents threads attachés à la file d'attente. Rappelez-vous qu'ils ne sont pas censés terminer la tâche car l'ordre dans lequel ils sont ajoutés à la file d'attente.L'ordre de la tâche diffère chaque fois que les threads sont créés comme nécessairement automatiquement.Les tâches sont exécutées en parallèle. Avec plus que cela (maxConcurrentOperationCount) est atteint, certaines tâches se comportent 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("task running in main thread")
            }else{
                print("task running in background thread")
            }
            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) completed downloading")
        }
        print("\(i) executed")
    }
}

La tâche peut s'exécuter dans le thread principal lorsque vous utilisez la synchronisation dans GCD. Sync exécute un bloc sur une file d'attente donnée et attend qu'il se termine, ce qui entraîne le blocage du thread principal ou de la file d'attente principale. Puisque la file d'attente principale doit attendre la fin du bloc distribué, le thread principal sera disponible pour traiter les blocs des files file d'attente principale Il y a donc une chance que le code s'exécutant sur la file d'attente d'arrière-plan s'exécute réellement sur le thread principal. Depuis sa file d'attente simultanée, les tâches peuvent ne pas se terminer dans l'ordre dans lequel elles sont ajoutées à la file d'attente. Mais avec un fonctionnement synchrone, il le fait bien qu'ils puissent être traités par différents threads. Donc, il se comporte comme il s'agit de la file d'attente série.

Voici un résumé de ces expériences

N'oubliez pas qu'en utilisant GCD, vous ajoutez uniquement une tâche à la file d'attente et effectuez une tâche à partir de cette file d'attente. La file d'attente distribue votre tâche dans le thread principal ou en arrière-plan selon que l'opération est synchrone ou asynchrone. Les types de files d'attente sont les files d'attente de distribution série, simultanée et principale.Toutes les tâches que vous effectuez sont effectuées par défaut à partir de la file d'attente de distribution principale.Il existe déjà quatre files d'attente simultanées globales prédéfinies pour votre application et une file d'attente principale (DispatchQueue.main). peut également créer manuellement votre propre file d'attente et effectuer une tâche à partir de cette file d'attente.

La tâche liée à l'interface utilisateur doit toujours être effectuée à partir du thread principal en distribuant la tâche à la file d'attente DispatchQueue.main.sync/asyncprincipale.

EDIT: Cependant, dans certains cas, vous devez effectuer des opérations d'appels réseau de manière synchrone dans un thread d'arrière-plan sans geler l'interface utilisateur (par exemple, rafraîchir le jeton OAuth et attendre si cela réussit ou non) .Vous devez envelopper cette méthode dans une opération asynchrone. les opérations sont exécutées dans l'ordre et sans blocage du 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) completed downloading")
            }
            print("\(i) executed")
        }
    }
}

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

LC 웃
la source
Grande démonstration .... ligne suivante n'attendez pas que le bloc s'exécute, ce qui entraîne un thread principal non bloquant, c'est pourquoi si vous utilisez des points d'arrêt sur un thread d'arrière-plan, il passera au }car il ne s'exécute vraiment pas à ce moment
Honey
@That paresseux iOS Guy 웃 Je ne comprends toujours pas la différence entre async concurrent et async série. Quelle est l'implication de l'utilisation de l'un ou l'autre. Ils fonctionnent tous les deux en arrière-plan sans perturber l'interface utilisateur. Et pourquoi utiliseriez-vous la synchronisation? Tout le code n'est pas synchronisé. l'un après l'autre?
eonist
1
@GitSyncApp, vous pouvez regarder la vidéo ici
Anish Parajuli 웃
@ Ce paresseux iOS Guy 웃: merci d'avoir fait ça. J'ai posté sur slack swift-lang. Ce serait 👌 Si vous pouviez en faire un sur DispatchGroup et DispatchWorkItem également. : D
eonist
Je l' ai testé votre dernier, la concurrentQueue.syncde doLongSyncTaskInConcurrentQueue()fonction, il imprime thread principal, Task will run in different threadne semble pas vrai.
gabbler
54

Tout d'abord, il est important de connaître la différence entre les threads et les files d'attente et ce que fait réellement GCD. Lorsque nous utilisons des files d'attente de répartition (via GCD), nous mettons vraiment en file d'attente, pas en thread. Le framework Dispatch a été conçu spécifiquement pour nous éloigner du threading, car Apple admet que «l'implémentation d'une solution de threading correcte [peut] devenir extrêmement difficile, voire [parfois] impossible à réaliser». Par conséquent, pour effectuer des tâches simultanément (tâches que nous ne voulons pas geler l'interface utilisateur), tout ce que nous devons faire est de créer une file d'attente de ces tâches et de la remettre à GCD. Et GCD gère tous les threads associés. Par conséquent, tout ce que nous faisons vraiment, c'est faire la queue.

La deuxième chose à savoir tout de suite est ce qu'est une tâche. Une tâche est tout le code de ce bloc de file d'attente (pas dans la file d'attente, car nous pouvons ajouter des éléments à une file d'attente tout le temps, mais dans la fermeture où nous l'avons ajouté à la file d'attente). Une tâche est parfois appelée un bloc et un bloc est parfois appelé une tâche (mais ils sont plus communément appelés tâches, en particulier dans la communauté Swift). Et peu importe la quantité ou le peu de code, tout le code entre les accolades est considéré comme une seule tâche:

serialQueue.async {
    // this is one task
    // it can be any number of lines with any number of methods
}
serialQueue.async {
    // this is another task added to the same queue
    // this queue now has two tasks
}

Et il est évident de mentionner que concourant signifie simplement en même temps avec d'autres choses et signifie série l'un après l'autre (jamais en même temps). Serialiser quelque chose, ou mettre quelque chose en série, signifie simplement l'exécuter du début à la fin dans son ordre de gauche à droite, de haut en bas, sans interruption.

Il existe deux types de files d'attente, série et simultanée, mais toutes les files d'attente sont simultanées l'une par rapport à l'autre . Le fait que vous souhaitiez exécuter n'importe quel code "en arrière-plan" signifie que vous souhaitez l'exécuter simultanément avec un autre thread (généralement le thread principal). Par conséquent, toutes les files d'attente de distribution, en série ou simultanées, exécutent leurs tâches simultanément par rapport aux autres files d'attente . Toute sérialisation effectuée par des files d'attente (par des files d'attente série) n'a à voir qu'avec les tâches de cette seule file d'attente de distribution [série] (comme dans l'exemple ci-dessus où il y a deux tâches dans la même file d'attente série; ces tâches seront exécutées une après l'autre, jamais simultanément).

SERIAL QUEUES (souvent appelées files d'attente de distribution privées) garantissent l'exécution des tâches une par une du début à la fin dans l'ordre dans lequel elles ont été ajoutées à cette file d'attente spécifique. C'est la seule garantie de sérialisation n'importe où dans la discussion des files d'attente d'expédition- que les tâches spécifiques dans une file d'attente série spécifique sont exécutées en série. Cependant, les files d'attente série peuvent s'exécuter simultanément avec d'autres files d'attente série s'il s'agit de files d'attente distinctes car, là encore, toutes les files d'attente sont concurrentes les unes par rapport aux autres. Toutes les tâches s'exécutent sur des threads distincts, mais toutes les tâches ne sont pas garanties de s'exécuter sur le même thread (pas important, mais intéressant à savoir). Et le framework iOS n'est pas livré avec des files d'attente série prêtes à l'emploi, vous devez les créer. Les files d'attente privées (non globales) sont en série par défaut, donc pour créer une file d'attente en série:

let serialQueue = DispatchQueue(label: "serial")

Vous pouvez le rendre concurrent grâce à sa propriété d'attribut:

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])

Mais à ce stade, si vous n'ajoutez aucun autre attribut à la file d'attente privée, Apple vous recommande d'utiliser simplement l'une de leurs files d'attente globales prêtes à l'emploi (qui sont toutes simultanées). Au bas de cette réponse, vous verrez une autre façon de créer des files d'attente série (à l'aide de la propriété cible), c'est ainsi qu'Apple recommande de le faire (pour une gestion plus efficace des ressources). Mais pour l'instant, l'étiqueter est suffisant.

Les QUEUES CONCURRENTES (souvent appelées files d'attente de répartition globales) peuvent exécuter des tâches simultanément; les tâches sont toutefois assurées de démarrer dans l'ordre dans lequel elles ont été ajoutées à cette file d'attente spécifique, mais contrairement aux files d'attente série, la file d'attente n'attend pas la fin de la première tâche avant de démarrer la deuxième tâche. Les tâches (comme avec les files d'attente série) s'exécutent sur des threads distincts et (comme avec les files d'attente série) toutes les tâches ne sont pas garanties de s'exécuter sur le même thread (pas important, mais intéressant à savoir). Et le framework iOS est livré avec quatre files d'attente simultanées prêtes à l'emploi. Vous pouvez créer une file d'attente simultanée en utilisant l'exemple ci-dessus ou en utilisant l'une des files d'attente globales d'Apple (ce qui est généralement recommandé):

let concurrentQueue = DispatchQueue.global(qos: .default)

RÉSISTANT AUX CYCLES DE RETENUE: les files d'attente de distribution sont des objets comptés par référence, mais vous n'avez pas besoin de conserver et de libérer les files d'attente globales car elles sont globales et, par conséquent, la conservation et la libération sont ignorées. Vous pouvez accéder directement aux files d'attente globales sans avoir à les affecter à une propriété.

Il existe deux façons de répartir les files d'attente: de manière synchrone et asynchrone.

SYNC DISPATCHING signifie que le thread où la file d'attente a été distribuée (le thread appelant) s'arrête après avoir distribué la file d'attente et attend que la tâche de ce bloc de file d'attente se termine avant de reprendre. Pour expédier de manière synchrone:

DispatchQueue.global(qos: .default).sync {
    // task goes in here
}

ASYNC DISPATCHING signifie que le thread appelant continue de s'exécuter après la distribution de la file d'attente et n'attend pas que la tâche de ce bloc de file d'attente se termine. Pour distribuer de manière asynchrone:

DispatchQueue.global(qos: .default).async {
    // task goes in here
}

Maintenant, on pourrait penser que pour exécuter une tâche en série, une file d'attente en série doit être utilisée, et ce n'est pas tout à fait correct. Afin d'exécuter plusieurs tâches en série, une file d'attente en série doit être utilisée, mais toutes les tâches (isolées par elles-mêmes) sont exécutées en série. Prenons cet exemple:

whichQueueShouldIUse.syncOrAsync {
    for i in 1...10 {
        print(i)
    }
    for i in 1...10 {
        print(i + 100)
    }
    for i in 1...10 {
        print(i + 1000)
    }
}

Quelle que soit la façon dont vous configurez (série ou simultanée) ou distribuez (synchronisation ou asynchrone) cette file d'attente, cette tâche sera toujours exécutée en série. La troisième boucle ne s'exécutera jamais avant la deuxième boucle et la deuxième boucle ne s'exécutera jamais avant la première boucle. Cela est vrai dans n'importe quelle file d'attente utilisant n'importe quelle expédition. C'est lorsque vous introduisez plusieurs tâches et / ou files d'attente que la série et la concurrence entrent vraiment en jeu.

Considérez ces deux files d'attente, une série et une simultanée:

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)

Disons que nous distribuons deux files d'attente simultanées en asynchrone:

concurrentQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
103
3
104
4
105
5

Leur sortie est confuse (comme prévu) mais notez que chaque file d'attente a exécuté sa propre tâche en série. Il s'agit de l'exemple le plus élémentaire de concurrence d'accès - deux tâches s'exécutant en même temps en arrière-plan dans la même file d'attente. Maintenant, faisons la première série:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

101
1
2
102
3
103
4
104
5
105

La première file d'attente n'est-elle pas censée être exécutée en série? C'était (et c'était le deuxième). Tout ce qui s'est passé en arrière-plan ne concerne pas la file d'attente. Nous avons dit à la file d'attente série de s'exécuter en série et c'est le cas ... mais nous ne lui avons donné qu'une seule tâche. Maintenant, donnons-lui deux tâches:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

Et c'est l'exemple le plus basique (et le seul possible) de sérialisation - deux tâches s'exécutant en série (l'une après l'autre) en arrière-plan (vers le thread principal) dans la même file d'attente. Mais si nous leur avons fait deux files d'attente série distinctes (car dans l'exemple ci-dessus, ce sont la même file d'attente), leur sortie est à nouveau brouillée:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue2.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
3
103
4
104
5
105

Et c'est ce que je voulais dire quand j'ai dit que toutes les files d'attente étaient concurrentes les unes par rapport aux autres. Ce sont deux files d'attente série exécutant leurs tâches en même temps (car ce sont des files d'attente distinctes). Une file d'attente ne connaît pas ou ne se soucie pas des autres files d'attente. Revenons maintenant à deux files d'attente série (de la même file d'attente) et ajoutons une troisième file d'attente, une simultanée:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 1000)
    }
}

1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005

C'est un peu inattendu, pourquoi la file d'attente simultanée a-t-elle attendu la fin des files d'attente série avant de s'exécuter? Ce n'est pas la concurrence. Votre terrain de jeu peut afficher une sortie différente, mais la mienne l'a montré. Et cela a montré cela parce que la priorité de ma file d'attente simultanée n'était pas assez élevée pour que GCD exécute sa tâche plus tôt. Donc, si je garde tout pareil mais que je change la QoS de la file d'attente globale (sa qualité de service, qui est simplement le niveau de priorité de la file d'attente) let concurrentQueue = DispatchQueue.global(qos: .userInteractive), alors le résultat est comme prévu:

1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105

Les deux files d'attente série ont exécuté leurs tâches en série (comme prévu) et la file d'attente simultanée a exécuté sa tâche plus rapidement car elle a reçu un niveau de priorité élevé (une QoS élevée ou une qualité de service).

Deux files d'attente simultanées, comme dans notre premier exemple d'impression, montrent une impression confuse (comme prévu). Pour les faire imprimer proprement en série, nous devions créer les deux la même file d'attente série (la même instance de cette file d'attente, pas seulement la même étiquette) . Ensuite, chaque tâche est exécutée en série par rapport à l'autre. Une autre façon, cependant, de les faire imprimer en série est de les garder tous les deux simultanés mais de changer leur méthode d'expédition:

concurrentQueue.sync {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

N'oubliez pas que la distribution de synchronisation signifie uniquement que le thread appelant attend que la tâche de la file d'attente soit terminée avant de continuer. La mise en garde ici, évidemment, est que le thread appelant est gelé jusqu'à ce que la première tâche soit terminée, ce qui peut ou non être la façon dont vous souhaitez que l'interface utilisateur fonctionne.

Et c'est pour cette raison que nous ne pouvons pas faire ce qui suit:

DispatchQueue.main.sync { ... }

Il s'agit de la seule combinaison possible de files d'attente et de méthodes de répartition que nous ne pouvons pas effectuer: répartition synchrone sur la file d'attente principale. Et c'est parce que nous demandons à la file d'attente principale de se figer jusqu'à ce que nous exécutions la tâche dans les accolades ... que nous avons envoyées à la file d'attente principale, que nous venons de geler. C'est ce qu'on appelle une impasse. Pour le voir en action dans une aire de jeux:

DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
    print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock

Une dernière chose à mentionner concerne les ressources. Lorsque nous attribuons une tâche à une file d'attente, GCD trouve une file d'attente disponible dans son pool géré en interne. En ce qui concerne l'écriture de cette réponse, il y a 64 files d'attente disponibles par qos. Cela peut sembler beaucoup mais ils peuvent être rapidement consommés, en particulier par des bibliothèques tierces, en particulier les frameworks de bases de données. Pour cette raison, Apple a des recommandations sur la gestion des files d'attente (mentionnées dans les liens ci-dessous); un étant:

Au lieu de créer des files d'attente simultanées privées, soumettez les tâches à l'une des files d'attente de distribution simultanées globales. Pour les tâches série, définissez la cible de votre file d'attente série sur l'une des files d'attente simultanées globales. De cette façon, vous pouvez conserver le comportement sérialisé de la file d'attente tout en minimisant le nombre de files d'attente distinctes créant des threads.

Pour ce faire, au lieu de les créer comme nous l'avons fait auparavant (ce que vous pouvez toujours), Apple recommande de créer des files d'attente série comme celle-ci:

let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))

Pour plus d'informations, je recommande ce qui suit:

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1

https://developer.apple.com/documentation/dispatch/dispatchqueue

bsod
la source
7

Si je comprends bien le fonctionnement de GCD, je pense qu'il existe deux types de DispatchQueue, serialet concurrent, en même temps, il y a deux manières de DispatchQueuerépartir ses tâches, l'attribution closure, la première est async, et l'autre sync. Ceux-ci déterminent ensemble la manière dont la fermeture (tâche) est réellement exécutée.

J'ai trouvé cela serialet je concurrentveux dire combien de threads cette file d'attente peut utiliser serialsignifie un, alors que cela concurrentsignifie plusieurs. Et syncet asyncsignifie que la tâche sera exécutée sur quel thread, le thread de l'appelant ou le thread sous-jacent à cette file d'attente, syncsignifie s'exécuter sur le thread de l'appelant alors que asyncsignifie s'exécuter sur le thread sous-jacent.

Ce qui suit est un code expérimental qui peut s'exécuter sur le terrain de jeu Xcode.

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("code Fragment begin")
  print("Task 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("code Fragment completed")
}

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
    /// Using an concurrent queue to simulate concurent task executions.
    cq.async {
      print("Caller Thread:\(Thread.current.description)")
      /// Serial Queue Async, tasks run serially, because only one thread that can be used by serial queue, the underlying thread of serial queue.
      //serialQueueAsync()
      /// Serial Queue Sync, tasks run serially, because only one thread that can be used by serial queue,one by one of the callers' threads.
      //serialQueueSync()
      /// Concurrent Queue Async, tasks run concurrently, because tasks can run on different underlying threads
      //concurrentQueueAsync()
      /// Concurrent Queue Sync, tasks run concurrently, because tasks can run on different callers' thread
      //concurrentQueueSync()
    }
  }
}
tasksExecution()

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

Keith
la source
7

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

Papa va avoir besoin d'aide

Imaginons que votre père fasse la vaisselle et que vous venez de prendre un verre de soda. Vous apportez le verre à votre père pour le nettoyer, en le mettant à côté de l'autre plat.

Maintenant, ton père fait la vaisselle tout seul, alors il va devoir les faire un par un: ton père ici représente une file d'attente en série .

Mais vous n'êtes pas vraiment intéressé à rester là et à le regarder se nettoyer. Alors, vous laissez tomber le verre et retournez dans votre chambre: c'est ce qu'on appelle une répartition asynchrone . Votre père pourrait vous le faire savoir ou non une fois qu'il a terminé, mais le plus important est que vous n'attendez pas que le verre soit nettoyé; tu retournes dans ta chambre pour faire, tu sais, des trucs pour enfants.

Supposons maintenant que vous ayez encore soif et que vous vouliez avoir de l'eau sur 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é. Alors, restez là et regardez votre père faire la vaisselle jusqu'à ce que la vôtre soit faite. Il s'agit d'une distribution de synchronisation , car vous êtes bloqué pendant que vous attendez que la tâche soit terminée.

Et enfin, disons que votre maman décide d'aider votre père et le rejoint pour faire la vaisselle. Maintenant, la file d'attente devient une file d'attente simultanée puisqu'ils peuvent nettoyer plusieurs plats en même temps; mais notez que vous pouvez toujours décider d'y attendre ou de retourner dans votre chambre, quel que soit leur fonctionnement.

J'espère que cela t'aides

Yunus Nedim Mehel
la source
3

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

RÉPONSE : - Les trois blocs exécutés l'un après l'autre, j'ai créé un exemple de code qui aide à comprendre.

let serialQueue = DispatchQueue(label: "SampleSerialQueue")
//Block first
serialQueue.async {
    for i in 1...10{
        print("Serial - First operation",i)
    }
}

//Block second
serialQueue.async {
    for i in 1...10{
        print("Serial - Second operation",i)
    }
}
//Block Third
serialQueue.async {
    for i in 1...10{
        print("Serial - Third operation",i)
    }
}
CrazyPro007
la source