Aujourd’hui nous allons parler de la programmation concurrente sous iOS (en Swift bien évidemment) avec le framework natif Grand Central Dispatch (GCD
). Si vous n’êtes pas familier avec la programmation concurrente je vous suggère de commencer par faire un tour du coté de wikipedia. Nous allons voir quelle philosophie est caché derrière et comment l’utiliser à bon escient.
Grand Central Dispatch
GCD
(Grand Central Dispatch) est une framework fournissant des APIs de bas niveau pour traiter la programmation concurrente au seins des plateformes iOS et OSX. Tous les frameworks de plus haut niveaux proposant de gérer la concurrence chez Apple, comme les NSOperation
, sont basés sur GCD
. Ses APIs permettent de diviser le travail d’un processus en différentes tâches individuelles, puis de nous aider à orchestrer l’exécution de ces tâches en parallèle ou en séquentielle à l’aide de file. Il permet de nous abstraire de la notion de thread, processeur, coeur, etc. et il optimise la gestion des resources par la même occasion avec son propre threadpool.
Les Queues
Une notion fondamentale sur laquelle repose GCD
est celle de file FIFO (First In, First Out). Les tâches que l’on veut executer vont d’abord être insérées dans ces files (queues) puis GCD va venir les récupérer individuellement pour les « dispatcher » aux bons threads/processeurs/coeurs. On peut distinguer 2 typologies de file :
- File concurrente (concurrent) : exécute plusieurs tâches en parallèle
- File séquentielle (serial) : exécute les tâches une à une en série
Voici les 3 catégories de file existantes :
Main queue
: équivalent au thread principal qui gère principalement l’UI de l’application.Global queue
: le système nous fournit 3 globals queues avec des priorités différentes (HIGH, DEFAULT et LOW). Ces 3 files sont de type concurrente.Custom queue
: des files d’attentes que l’on peut créer nous même et du type que l’on veut (séquentielle ou concurrente).
En pratique
Commençons par créer une des 3 catégories de file:
// Main queue
let mainQueue = dispatch_get_main_queue()
// Global queue: HIGH, DEFAULT ou LOW
let globalDefaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// Custom queue: SERIAL ou CONCURRENT
let customSerialQueue = dispatch_queue_create("fr.swift-tuto.queue", DISPATCH_QUEUE_SERIAL)
Revenons un peu sur la récupération d’une des global queue ou de la création d’une custom queue qui demande un peu plus d’explication que juste une ligne de code. Pour récupérer une global queue on utilise la méthode dispatch_get_global_queue
avec comme premier paramètre un des 3 niveaux de priorité et le second paramètre est toujours 0 car non utilisé pour le moment. Pour la création d’une custom queue, on définit d’abord un identifiant (il est recommandé d’utiliser le format reverse-DNS) puis la typologie de queue.
Maintenant que nous savons comment créer des files, comment les utilisons nous me direz vous ? C’est très simple, grâce au « dispatcher » ! Voici un petit example :
let lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
print("before")
dispatch_async(lowQueue) {
print("inside first block")
let html = getRemoteHTMLAt("http://swift-tuto.fr")
dispatch_async(dispatch_get_main_queue()) {
print("inside second block")
// Toujours utiliser les composants UIKit dans le thread principal
myWebView.loadHTMLString(html, baseURL: nil)
print("finished!")
}
}
print("after")
// La console va afficher ceci dans cet ordre :
// before
// after
// inside first block
// inside second block
// finished!
Essayons de décomposer tous ça. On récupère pour commencer une file de priorité basse. Ensuite nous « dispatchons » un block de code de manière asynchrone sur la file de basse priorité récupéré auparavant. Ce block de code var récupérer de manière synchrone une chaîne HTML pour la stocker dans la variable html
. Pour finir nous mettons à jour notre myWebView
dans le thread principal avec le HTML récupérer. Il est important d’effectuer la dernière étape dans le thread principal car UIKit
n’est pas threadsafe et si vous mettez à jour vos composants dans un autre thread votre application risque de planter.
Pour aller plus loin…
Avec GCD
vous pouvez gérer tous ce qui concerne la programmation concurrente. Nous allons voir dans cette sections différent cas d’utilisation un peu plus poussé.
Par example vous avez plusieurs tâches à exécuter et vous souhaitez attendre que l’ensemble de ces tâches soient effectué avant de faire autre chose. Pour cela il faut utiliser les dispatch_group_t
:
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
let group = dispatch_group_create()
for _ in 0 ..< 10 {
dispatch_group_enter(group)
dispatch_async(queue) {
// Long job process
dispatch_group_leave(group)
}
}
dispatch_group_notify(group, queue) {
print("All jobs done!")
}
Afin d'attendre que l'ensemble des tâches soient effectuées on créer un group
et dans notre boucle nous définissons quand une tâche commence dans le groupe et surtout quand la tâche est fini. Il faut imaginer cela comme un compteur, à chaque dispatch_group_enter
ce compteur est incrémenté de 1, et à chaque dispatch_group_leave
il est décrémenté de 1. A la fin nous définissons un dispatch_group_notify
qui écoute le compteur du group
et qui exécute le block dans la queue
donnée quand il arrive à 0.
Un deuxième cas très utile peut être le fait de vouloir exécuter du code après un certain temps. Si vous avez ce genre de cas pensez tout de suite à GCD
et sa méthode dispatch_after
:
let twoSeconds = dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_SEC))
let queue = dispatch_get_main_queue()
print("before")
dispatch_after(twoSeconds, queue) {
print("2 seconds after")
}
print("after")
// Affiche
// before
// after
// 2 seconds after
Ici la partie la plus compliqué est la définition du temps après lequel vous souhaitez exécuté votre block de code avec un dispatch_time_t
.
Conclusion
GCD
est un framework très puissant qui permet de réellement s'abstraire des contraintes de bas niveaux inhérente à la programmation concurrente. Cependant il demande un peu de travail avant de pouvoir en saisir toute sa puissance et éviter les erreurs de débutant. Si vous avez des questions, n'hésitez pas à nous laisser des commentaires.