Maîtriser les coroutines Kotlin : Guide technique et mise en œuvre

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() ou join().
  • 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")
}

Étiquettes: kotlin coroutines Asynchronous JVM multithreading

Publié le 4 septembre à 20h48