Concurrence et multithreading en Kotlin : niveau avancé
Dans le développement moderne, une gestion efficace de la concurrence est la clé pour créer des applications rapides et réactives. Kotlin, grâce à son support intégré des coroutines, offre une boîte à outils puissante et flexible qui va bien au-delà des threads Java standard. Dans cet article, nous explorerons les concepts avancés de concurrence et de multithreading en Kotlin, y compris le travail avec Channel, Flow, Mutex et les subtilités des planificateurs.
1. Coroutines : plus en profondeur que les bases
Les coroutines sont des threads légers qui permettent d'écrire du code asynchrone dans un style impératif. Cependant, au niveau avancé, il est important de comprendre comment fonctionnent les dispatchers et les contextes de coroutines.
Planificateurs (Dispatchers)
Le choix du bon dispatcher est crucial pour les performances :
Dispatchers.Default— pour les tâches intensives en CPU (utilise par défaut un pool de threads égal au nombre de cœurs du processeur).Dispatchers.IO— pour les opérations d'entrée/sortie (réseau, fichiers). Le pool de threads peut s'étendre jusqu'à 64 threads (ou plus, selon la configuration).Dispatchers.Main— pour le thread UI (Android, JavaFX).newSingleThreadContextetnewFixedThreadPoolContext— pour créer vos propres pools.
import kotlinx.coroutines.*
fun main() = runBlocking { launch(Dispatchers.Default) { println("Tâche CPU sur ${Thread.currentThread().name}") } launch(Dispatchers.IO) { println("Tâche E/S sur ${Thread.currentThread().name}") }}Concurrence structurée
L'une des principales fonctionnalités de Kotlin est la concurrence structurée. Elle garantit que les coroutines enfants se terminent avant la fin de la coroutine parente. Cela évite les fuites mémoire et simplifie la gestion du cycle de vie.
fun main() = runBlocking { coroutineScope { launch { delay(1000) println("Tâche 1 terminée") } launch { delay(500) println("Tâche 2 terminée") } } println("Toutes les tâches terminées")}2. Canaux (Channel) pour le transfert de données entre coroutines
Channel est l'équivalent d'une file d'attente bloquante (BlockingQueue), mais pour les coroutines. Il permet de transférer des données en toute sécurité entre coroutines, en suspendant l'expéditeur si le canal est plein, ou le récepteur si le canal est vide.
Types de canaux
- RendezvousChannel (par défaut) — n'a pas de tampon. L'envoi est bloqué jusqu'à la réception.
- BufferedChannel — possède un tampon de taille définie.
- ConflatedChannel — ne stocke que la dernière valeur envoyée, en écrasant les précédentes.
- UnlimitedChannel — tampon illimité (attention : peut provoquer un OOM).
import kotlinx.coroutines.*import kotlinx.coroutines.channels.*
fun main() = runBlocking { val channel = Channel<Int>(capacity = 5) // Canal tamponné
launch { for (i in 1..10) { channel.send(i) println("Envoyé : $i") } channel.close() }
launch { for (value in channel) { println("Reçu : $value") delay(100) } }}Fan-out et Fan-in
Les canaux sont parfaits pour les modèles "fan-out" (un producteur, plusieurs consommateurs) et "fan-in" (plusieurs producteurs, un consommateur).
3. Flow : flux de données réactifs
Contrairement à Channel, qui est "chaud" (les données sont envoyées indépendamment de la présence d'un abonné), Flow est un flux "froid" qui