Contactez-nous

Lancer une tâche avec `launch`

Apprenez à utiliser le constructeur de coroutine `launch` en Kotlin pour démarrer des tâches asynchrones en arrière-plan sans bloquer (fire-and-forget).

Introduction : Démarrer des tâches sans attendre

Maintenant que nous savons ce qu'est une fonction `suspend` et qu'elle doit être appelée depuis un contexte de coroutine, la question naturelle est : comment créer ce contexte et démarrer effectivement une coroutine pour exécuter notre code suspendable ?

Kotlin fournit plusieurs constructeurs de coroutines pour cela. L'un des plus fondamentaux et des plus utilisés est `launch`. Son rôle principal est de lancer une nouvelle coroutine qui exécute une tâche spécifique en arrière-plan, de manière concurrente avec le reste du code, sans nécessairement attendre un résultat de cette tâche. C'est souvent décrit comme un mécanisme de "fire-and-forget" : on lance la tâche et on continue.

`launch` est l'outil idéal lorsque vous voulez démarrer une opération asynchrone (comme mettre à jour une base de données, envoyer des logs, effectuer un calcul en arrière-plan) dont vous n'avez pas besoin du résultat immédiatement.

Présentation et syntaxe de `launch`

`launch` est une fonction d'extension définie sur l'interface `CoroutineScope`. Cela signifie que vous devez toujours l'appeler dans le contexte d'une portée de coroutine existante (nous verrons `runBlocking` ou des portées spécifiques comme `GlobalScope` ou celles liées aux cycles de vie Android plus tard).

Sa signature de base simplifiée est :

`fun CoroutineScope.launch(context: CoroutineContext = ..., start: CoroutineStart = ..., block: suspend CoroutineScope.() -> Unit): Job`

Décortiquons les éléments importants :

  • `CoroutineScope.launch`: Indique que `launch` doit être appelée sur une instance de `CoroutineScope`.
  • `block: suspend CoroutineScope.() -> Unit`: C'est le paramètre principal, une lambda suspendable. C'est le code que votre nouvelle coroutine va exécuter. Notez que cette lambda est aussi une extension sur `CoroutineScope`, vous pouvez donc lancer d'autres coroutines depuis l'intérieur si nécessaire.
  • Retourne `Job`: `launch` retourne immédiatement (elle ne bloque pas l'appelant) un objet de type `Job`. Ce `Job` est une référence à la coroutine qui vient d'être lancée, permettant de la gérer (l'annuler, attendre sa complétion, etc.).
  • `context` et `start` (Optionnels): Permettent de personnaliser le contexte (par exemple, le `Dispatcher` sur lequel exécuter la coroutine) et la manière dont elle démarre (par défaut, elle démarre immédiatement).

Exemple simple : Lancer et oublier

Voyons `launch` en action. Pour exécuter cet exemple dans une fonction `main` standard (qui n'est pas `suspend`), nous utilisons `runBlocking`. `runBlocking` est un constructeur qui crée une coroutine et bloque le thread courant jusqu'à ce que toutes les coroutines lancées à l'intérieur (y compris celles lancées avec `launch`) soient terminées. C'est utile pour les fonctions `main` ou les tests, mais à éviter dans du code bloquant l'UI ou sur un serveur.

import kotlinx.coroutines.* // Import nécessaire

fun main() = runBlocking { // Crée une portée et attend la fin
    println("Main: Début du programme sur thread ${Thread.currentThread().name}")

    // Lance une nouvelle coroutine
    val job1: Job = launch {
        println("  Coroutine 1: Démarrée sur thread ${Thread.currentThread().name}")
        delay(1000L) // Fonction suspend, libère le thread pendant 1s
        println("  Coroutine 1: Terminée après 1s")
    }

    // Lance une autre coroutine
    val job2 = launch {
        println("    Coroutine 2: Démarrée sur thread ${Thread.currentThread().name}")
        delay(500L)
        println("    Coroutine 2: Terminée après 0.5s")
    }

    println("Main: Coroutines lancées. job1 actif: ${job1.isActive}, job2 actif: ${job2.isActive}")
    println("Main: Attente implicite par runBlocking...")

    // Pas besoin de job1.join() ou job2.join() ici car runBlocking attend déjà
    // Si ce n'était pas dans runBlocking, il faudrait attendre les jobs pour ne pas terminer le main trop tôt.

    println("Main: Fin du programme")
}
Sortie probable (l'ordre des messages des coroutines peut varier légèrement) :
Main: Début du programme sur thread main
Main: Coroutines lancées. job1 actif: true, job2 actif: true
Main: Attente implicite par runBlocking...
  Coroutine 1: Démarrée sur thread main
    Coroutine 2: Démarrée sur thread main
    Coroutine 2: Terminée après 0.5s
  Coroutine 1: Terminée après 1s
Main: Fin du programme

On voit que `launch` retourne immédiatement, permettant au `main` de continuer. Les coroutines s'exécutent ensuite de manière concurrente (ici, sur le même thread grâce à `delay` qui est non bloquant). `runBlocking` assure que le programme principal attend leur achèvement.

Le `Job` : Contrôler la coroutine

Le `Job` retourné par `launch` est une poignée sur la coroutine. Il permet d'interagir avec elle :

  • `job.join()` : C'est une fonction `suspend`. L'appeler suspend la coroutine courante jusqu'à ce que la coroutine représentée par `job` termine son exécution (soit normalement, soit par annulation). C'est utile lorsque vous avez besoin d'attendre la fin d'une tâche lancée en arrière-plan avant de continuer.
  • `job.cancel()` : Demande l'annulation de la coroutine. L'annulation dans les coroutines est coopérative. La coroutine doit vérifier périodiquement si elle a été annulée (par exemple, en appelant une fonction `suspend` de la bibliothèque comme `delay` ou `yield`, ou en vérifiant `isActive` explicitement) pour s'arrêter proprement. Si la coroutine exécute une boucle de calcul longue sans point de suspension, `cancel()` pourrait ne pas avoir d'effet immédiat.
  • `job.cancelAndJoin()` : Combine `cancel()` et `join()`.
  • Propriétés d'état : `isActive`, `isCompleted`, `isCancelled` permettent de vérifier l'état actuel du Job.
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(5) { i ->
                println("Coroutine: Je travaille ($i)... sur ${Thread.currentThread().name}")
                delay(500L)
            }
        } finally {
            println("Coroutine: Je suis terminée (peut-être annulée).")
        }
    }

    delay(1300L) // Laisse la coroutine tourner un peu
    println("Main: Je suis fatigué d'attendre !")
    job.cancel() // Annule le job
    println("Main: Annulation demandée.")
    // job.join() // Attendrait la fin de la coroutine (y compris son bloc finally)
    job.cancelAndJoin() // Demande l'annulation et attend qu'elle soit complète
    println("Main: Coroutine terminée.")
}
Sortie probable :
Coroutine: Je travaille (0)... sur main
Coroutine: Je travaille (1)... sur main
Coroutine: Je travaille (2)... sur main
Main: Je suis fatigué d'attendre !
Main: Annulation demandée.
Coroutine: Je suis terminée (peut-être annulée).
Main: Coroutine terminée.

Concurrence Structurée et `CoroutineScope`

Un aspect fondamental des coroutines est la concurrence structurée. Chaque coroutine est lancée dans un `CoroutineScope`. Cette portée définit le cycle de vie de la coroutine. Si la portée est annulée, toutes les coroutines lancées dans cette portée sont automatiquement annulées. Cela évite les fuites de coroutines (tâches qui continuent de tourner alors qu'elles ne sont plus nécessaires).

Lorsque vous utilisez `launch { ... }`, la nouvelle coroutine hérite du contexte (et notamment du `Dispatcher`, qui détermine sur quel(s) thread(s) elle s'exécute) de la `CoroutineScope` parente, mais elle est aussi liée à son cycle de vie. C'est ce qui fait que `runBlocking` attend les `launch` internes : ils font partie de sa portée.

Dans les applications réelles (Android, Ktor), vous utiliserez des portées spécifiques (comme `viewModelScope`, `lifecycleScope`, ou des portées créées manuellement) pour lancer vos coroutines, assurant ainsi qu'elles sont correctement gérées et annulées lorsque le composant associé (ViewModel, Activity, requête) est détruit ou terminé.

Quand utiliser `launch` ?

Utilisez `launch` lorsque vous souhaitez démarrer une tâche qui :

  • Peut s'exécuter indépendamment du code principal.
  • N'a pas besoin de retourner directement une valeur à l'appelant (fire-and-forget).
  • Effectue des opérations comme :
    • Mises à jour de l'interface utilisateur (sur le bon dispatcher).
    • Envoi de données analytiques ou de logs.
    • Déclenchement d'opérations de sauvegarde en arrière-plan.
    • Pré-chargement de données non essentiel immédiatement.

Si vous avez besoin d'obtenir un résultat de votre tâche asynchrone pour l'utiliser dans le code appelant, vous utiliserez plutôt le constructeur `async`, qui retourne un `Deferred` (similaire à une Future ou Promise).

Récapitulatif : Lancer des tâches concurrentes

Le constructeur de coroutine `launch` est essentiel pour démarrer des tâches asynchrones :

  • Doit être appelé sur un `CoroutineScope`.
  • Lance une nouvelle coroutine qui exécute la lambda `suspend` fournie.
  • Ne bloque pas l'appelant et s'exécute de manière concurrente.
  • Retourne immédiatement un `Job` pour gérer la coroutine (attendre avec `join()`, annuler avec `cancel()`).
  • Idéal pour les tâches en arrière-plan dont le résultat n'est pas immédiatement requis (fire-and-forget).
  • Participe à la concurrence structurée via la `CoroutineScope`.

C'est l'outil de base pour initier le travail asynchrone non bloquant en Kotlin.