Concurrence et multithreading en Kotlin : de A à Z
Les applications modernes nécessitent de plus en plus souvent l'exécution de plusieurs tâches simultanément. En Kotlin, comme dans d'autres langages JVM, le multithreading est traditionnellement implémenté via la classe Thread. Cependant, le langage propose un outil bien plus puissant et élégant : les coroutines. Dans cet article, nous aborderons tous les aspects de la concurrence et du multithreading en Kotlin : des concepts de base aux techniques avancées.
1. Bases du multithreading : Thread et Runnable
Commençons par l'approche classique. En Kotlin, comme en Java, vous pouvez créer un thread directement :
fun main() { val thread = Thread { for (i in 1..5) { println("Thread : $i") Thread.sleep(500) } } thread.start() println("Thread principal terminé")}Cependant, la gestion manuelle des threads présente des inconvénients :
- Frais généraux élevés — la création et la destruction de threads sont coûteuses.
- Complexité de synchronisation — il est facile de provoquer une race condition.
- Nombre limité de threads — le système ne peut créer qu'un nombre limité de threads.
Pour synchroniser l'accès aux données partagées, on utilise le mot-clé synchronized ou les verrous ReentrantLock :
class Counter { private var count = 0 @Synchronized fun increment() { count++ }}2. Les coroutines — une approche moderne de la concurrence
Les coroutines sont des threads légers qui résolvent les problèmes du multithreading classique. Elles ne sont pas liées aux threads système et peuvent s'exécuter sur un seul thread, en basculant entre les tâches.
2.1. Première coroutine
Pour travailler avec les coroutines, vous devez ajouter la dépendance org.jetbrains.kotlinx:kotlinx-coroutines-core. Exemple d'une coroutine simple :
import kotlinx.coroutines.*
fun main() = runBlocking { launch { delay(1000L) println("Monde !") } println("Bonjour,")}Ici, runBlocking est un pont entre le code normal et les coroutines. launch démarre une nouvelle coroutine, et delay est l'équivalent de Thread.sleep(), mais sans bloquer le thread.
2.2. Fonctions suspend
Le mot-clé suspend permet de suspendre l'exécution d'une fonction sans bloquer le thread :
suspend fun fetchData(): String { delay(2000L) // simulation d'une requête réseau return "Données reçues"}
fun main() = runBlocking { val result = async { fetchData() } println("Attente...") println(result.await())}2.3. Concurrence structurée
Kotlin prend en charge la concurrence structurée — toutes les coroutines enfants se terminent automatiquement avant la fin de la coroutine parente :
fun main() = runBlocking { coroutineScope { launch { delay(1000L) println("Tâche 1") } launch { delay(500L) println("Tâche 2") } } println("Toutes les tâches sont terminées")}3. Techniques avancées : Channel, Flow et Dispatchers
3.1. Canaux (Channel)
Les canaux permettent d'échanger des données entre les coroutines. C'est l'équivalent d'une file d'attente :
fun main() = runBlocking { val channel = Channel<Int>() launch { for (i in 1..5) { channel.send(i * i) } channel.close() } for (value in channel) { println(value) }}3.2. Flow — flux de données froids
Flow est un flux réactif qui émet des valeurs à la demande :
fun simpleFlow(): Flow<Int> = flow { for (i in 1..3) { delay(