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