Configuration de l'environnement
Pour intégrer les coroutines dans un projet Kotlin, la méthode la plus courante consiste à utiliser Gradle. Ajoutez la dépendance suivante à votre fichier build.gradle :
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
Pour des tests rapides via la ligne de commande avec un script .kts, vous pouvez inclure le JAR manuellement lors de la compilation :
kotlinc -classpath kotlinx-coroutines-core.jar -script script_coroutine.kts
Principes de base et exécution
Le bloc runBlocking est un constructeur de coroutine qui bloque le thread courant jusqu'à ce que son contenu soit terminé. Par défaut, sans appel explicite à des fonctions de lancement, l'exécution reste séquentielle.
import kotlinx.coroutines.*
fun actionA() {
println("Début Action A sur ${Thread.currentThread().name}")
println("Fin Action A")
}
fun actionB() {
println("Début Action B sur ${Thread.currentThread().name}")
println("Fin Action B")
}
fun main() = runBlocking {
println("Initialisation")
actionA()
actionB()
println("Terminé")
}
Pour obtenir une exécution asynchrone, on utilise le constructeur launch. Contrairement à l'appel direct, launch démarre une nouvelle coroutine sans bloquer le flux principal immédiatement.
runBlocking {
launch { actionA() }
launch { actionB() }
println("Appel des actions lancé...")
}
Suspension et fonctions suspendues
Le mot-clé suspend indique qu'une fonction peut interrompre son exécution sans bloquer le thread sous-jacent. Des fonctions comme yield() ou delay() permettent de redonner la main au dispatcher pour exécuter d'autres tâches en attente.
suspend fun sequenceAlpha() {
println("Alpha : Étape 1")
yield() // Cède le thread
println("Alpha : Étape 2")
}
suspend fun sequenceBeta() {
println("Beta : Étape 1")
yield()
println("Beta : Étape 2")
}
fun main() = runBlocking {
launch { sequenceAlpha() }
launch { sequenceBeta() }
}
Gestion des Dispatchers et Contextes
Par défaut, les coroutines s'exécutent dans le contexte du parent. Cependant, il est crucial de spécifier un Dispatcher selon le type de tâche :
- Dispatchers.Default : Optimisé pour les calculs intensifs (CPU).
- Dispatchers.IO : Optimisé pour les entrées/sorties (Réseau, Fichiers).
- Dispatchers.Main : Pour les interactions avec l'interface utilisateur (Android/Swing).
Il est également possible d'utiliser un pool de threads personnalisé via un Executor Java :
import java.util.concurrent.Executors
val monContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
runBlocking {
launch(monContext) {
println("Exécution sur un thread dédié : ${Thread.currentThread().name}")
}
}
Contrôle du démarrage avec CoroutineStart
Le paramètre start de la fonction launch permet de modifier le comportement initial :
DEFAULT: Planifie immédiatement l'exécution.LAZY: Ne démarre que lors d'un appel explicite àstart()oujoin().UNDISPATCHED: Commence l'exécution immédiatement sur le thread actuel jusqu'au premier point de suspension.
launch(start = CoroutineStart.LAZY) {
// Ce code ne s'exécutera pas tant qu'on n'appelle pas job.start()
}
Différences entre launch et async
Bien que similaires, ces deux constructeurs servent des objectifs différents :
| Caractérsitique | launch | async |
|---|---|---|
| Retour | Retourne un objet Job. |
Retourne un Deferred<T>. |
| Valeur | Pas de valeur de retour (Fire and forget). | Permet de récupérer un résultat via await(). |
| Exceptions | Propagées immédiatement au parent. | Encapsulées et jetées lors de l'appel à await(). |
Gestion des erreurs
Pour async, l'usage du bloc try-catch autour de await() est la pratique stendard :
val result = async {
// Risque d'exception
calculCritique()
}
try {
val data = result.await()
} catch (e: Exception) {
println("Erreur interceptée : ${e.message}")
}
Pour launch, on utilise souvent un CoroutineExceptionHandler pour capturer les erreurs de manière globale au sein d'un scope :
val gestionnaire = CoroutineExceptionHandler { _, exception ->
println("Exception capturée : ${exception.localizedMessage}")
}
val scope = CoroutineScope(Job() + Dispatchers.Main + gestionnaire)
scope.launch {
throw IllegalStateException("Échec du processus")
}