Channel : qu'est-ce que c'est, types de canaux et coroutines en Kotlin

Auteur : IT Sectr Publié le : 2026-03-17 Temps de lecture : 8 min

Channel est un primitif de synchronisation de la bibliothèque Kotlin Coroutines pour transférer des données entre coroutines. Selon Kotlin Documentation, 2025, Channel implémente le modèle producteur-consommateur avec envoi bloquant via des fonctions suspend. Channel prend en charge les modes Rendezvous, Buffered et Conflated, chacun définissant le comportement en cas de débordement.

Points clés

  • Channel est un primitif de transfert de données entre coroutines de kotlinx.coroutines, basé sur le modèle producteur-consommateur
  • Rendezvous Channel — sans tampon : send() est suspendu jusqu'à ce que receive() soit appelé
  • Buffered Channel — avec un tampon de capacité définie, send() est suspendu lorsqu'il est plein
  • Conflated Channel — stocke uniquement la dernière valeur, les anciennes sont ignorées en cas de débordement
  • Channel est la base pour construire des flux chauds, callbackFlow et des modèles d'acteur

Qu'est-ce que Channel en Kotlin ?

Channel est conceptuellement similaire à BlockingQueue de Java, mais avec des fonctions suspend send() et receive() au lieu de put() et take() bloquants. Un développeur Kotlin utilise Channel pour organiser l'échange de données entre coroutines sans synchronisation via la mémoire partagée. Le canal garantit une livraison ordonnée — l'ordre d'envoi correspond à l'ordre de réception.

Création d'un canal

Pour créer un Channel, la fonction d'usine Channel<T>(capacity) est appelée. Le paramètre capacity détermine le type de canal : RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) ou un nombre spécifique. Le type d'élément T est spécifié via les génériques. Fermer le canal avec close() signale qu'il n'y aura plus de nouveaux éléments.

Send et Receive

send(value) est une fonction suspend qui suspend la coroutine émettrice si le canal est plein. receive() est une fonction suspend qui suspend le récepteur si le canal est vide. Les alternatives trySend() et tryReceive() sont des versions non bloquantes qui retournent Boolean ou null lorsque l'opération n'est pas possible. Elles sont utiles dans des contextes non suspend.

Types de Channel dans kotlinx.coroutines

Kotlin fournit quatre variantes de Channel via la capacité du tampon : Rendezvous (capacité 0), Buffered (capacité N), Conflated (capacité 1, écrasement) et Unlimited (capacité Int.MAX_VALUE). Chaque type résout sa propre tâche, de la synchronisation stricte à la mise en tampon massive de données.

Rendezvous Channel est le plus strict : send() se bloque jusqu'à ce que receive() soit appelé dans une autre coroutine. Il s'agit essentiellement d'un point de rendez-vous de deux coroutines. Idéal pour une poignée de main stricte lorsque l'émetteur doit attendre que le récepteur traite l'élément. La perte de données est exclue — send ne se termine pas tant que receive n'est pas exécuté.

Conflated Channel stocke uniquement la dernière valeur envoyée. Si l'émetteur place un nouvel élément avant que le récepteur ne récupère l'ancien, l'ancien est ignoré. Conflated Channel est utile pour l'état de l'interface utilisateur : si un utilisateur change rapidement le curseur, les valeurs intermédiaires peuvent être ignorées et seule la dernière traitée.

Producteur-Consommateur avec les canaux

Le modèle classique Producteur-Consommateur sur Channel est implémenté via des coroutines parallèles. Producteur appelle send(value) dans une boucle, consommateur appelle receive(value). Le producteur et le consommateur peuvent travailler sur différents Dispatchers : producteur sur Dispatchers.IO, consommateur sur Dispatchers.Main. Channel synchronise automatiquement l'accès sans Lock ni synchronized.

Fan-out — plusieurs consommateurs sur un seul canal. Chaque élément va exactement à un consommateur (distribution round-robin). Fan-in — plusieurs producteurs écrivent dans un seul canal. Les coroutines émettrices rivalisent pour l'envoi, mais l'ordre des éléments est préservé. Les deux scénarios ne nécessitent pas de synchronisation supplémentaire.

Produce est un constructeur de coroutines qui crée un canal avec fermeture automatique. La fonction produce { } retourne un ReceiveChannel — un canal en lecture seule pour le consommateur. À l'intérieur du constructeur, send() envoie des données, et lorsque le bloc se termine ou qu'une exception se produit, le canal se ferme automatiquement, évitant les fuites.

Select et multiplexage

La bibliothèque kotlinx.coroutines fournit select — une expression qui attend le premier canal terminé parmi plusieurs alternatives. Select permet de multiplexer plusieurs canaux : par exemple, attendre des données de deux sources et traiter celle qui a répondu en premier. Syntaxe — select<T> { channel1.onReceive { } channel2.onReceive { } }. C'est une alternative à l'opérateur amb dans Rx.

Exemples de code Channel

Le premier exemple est un simple Rendezvous Channel où l'émetteur attend la réception :

kotlin
val channel = Channel<String>()

scope.launch {
    channel.send("Hello")
    println("Envoyé")
}

scope.launch {
    val msg = channel.receive()
    println("Reçu : $msg")
}

Le deuxième exemple — plusieurs consommateurs sur un seul canal (fan-out) :

kotlin
val channel = Channel<Int>(Channel.UNLIMITED)

scope.launch {
    for (x in 1..10) channel.send(x)
    channel.close()
}

repeat(2) { id ->
    scope.launch {
        for (msg in channel) {
            println("Consumer #$id : $msg")
        }
    }
}

Le troisième exemple — utilisation du constructeur produce avec gestion d'erreurs :

kotlin
val source = produce {
    for (i in 1..5) {
        delay(200)
        send(i)
    }
}

scope.launch {
    source
        .consumeAsFlow()
        .catch { println("Erreur : $it") }
        .collect { println("Élément : $it") }
}

Channel vs Flow

Channel est un primitif chaud : les données sont émises indépendamment des abonnés. Flow est froid : les données sont générées lors de l'abonnement. Channel prend en charge plusieurs producteurs et consommateurs avec livraison garantie de chaque élément à un consommateur (fan-out). Flow n'est pas conçu pour plusieurs producteurs indépendants.

Channel utilise un tampon avec capacité configurable et des fonctions suspend send/receive pour la gestion de la contre-pression. Flow utilise le mécanisme suspend collect avec contre-pression automatique via les coroutines. Channel est un outil de bas niveau pour des scénarios spécifiques : conversion de callbacks, modèle d'acteur, file d'attente de tâches avec plusieurs expéditeurs.

Pour les scénarios quotidiens dans Android (état de l'interface utilisateur, flux réactifs depuis la base de données), Google recommande Flow plutôt que Channel. Channel doit être utilisé lorsque un échange de données chaud entre coroutines avec un contrôle précis du tampon est nécessaire, ou lors de la conversion d'interfaces callback via callbackFlow, dont l'implémentation interne utilise Channel.

Un exemple pratique important : lors de l'implémentation d'un client WebSocket, Channel permet d'écrire des messages depuis une coroutine et de lire depuis une autre avec la garantie que chaque message sera traité exactement une fois. Flow n'est pas adapté à cette tâche car il est froid et ne prend pas en charge plusieurs producteurs. Channel avec capacité UNLIMITED garantit que les messages entrants ne sont pas perdus lors des retards temporaires du consommateur.

La gestion du cycle de vie du canal est une partie importante du travail avec Channel. Le canal doit être fermé lorsque toutes les données ont été envoyées afin que le consommateur puisse terminer l'itération. L'appel de channel.close() signale qu'il n'y aura plus de nouveaux éléments. Le consommateur peut itérer via for (item in channel) — la boucle se termine automatiquement après close() et la vidange du tampon. Alternativement, le consommateur peut appeler receive() dans une boucle avec gestion de ClosedReceiveChannelException.

Channel est activement utilisé dans Android pour implémenter EventBus sans dépendances : un Channel<Event> global avec une stratégie Broadcast permet d'envoyer des événements depuis n'importe quel point de l'application. Contrairement aux bus basés sur LiveData, Channel n'est pas lié au cycle de vie et ne nécessite pas de nettoyage lors de la transition entre écrans. send() depuis ViewModel et receive() dans Activity/Fragment via lifecycleScope fournissent une communication type-safe sans classes Event. Plusieurs consommateurs sur un Channel répartissent la charge — chaque élément est traité une fois, ce qui évite le traitement en double d'un seul événement dans différents abonnés.

Dans les systèmes d'acteurs, Channel sert de base pour implémenter une boîte aux lettres — une file d'attente de messages pour l'acteur. Un acteur est une coroutine qui lit les messages d'un Channel en boucle et les traite séquentiellement. Cette approche garantit que chaque message est traité dans l'ordre d'envoi, sans conditions de course. Kotlin n'a pas d'acteur intégré comme type (contrairement à Akka), mais Channel + launch est un remplacement léger.

Pour l'échange bidirectionnel, paires de canaux sont utilisées : un canal pour les requêtes du client au serveur, le second pour les réponses du serveur au client. Par exemple, lors de l'implémentation d'un Pipe dans une application multithread : le producteur écrit dans OutputChannel, le consommateur lit depuis InputChannel. Les fonctions suspend send et receive garantissent que le Producteur-Consommateur ne débordera pas la pile d'appels, car les coroutines se suspendent au lieu de bloquer. Channel avec capacité BUFFERED convient à la plupart des scénarios où la vitesse du producteur et du consommateur est à peu près égale. Pour les scénarios asymétriques, utilisez UNLIMITED afin que le producteur ne se suspende pas lorsque le consommateur est occupé — cela réduit le risque de blocage mais augmente la consommation mémoire.

Choix de la capacité du Channel

Lors de la conception d'une architecture avec des canaux, il est important de se souvenir de capacity : le choix de la capacité affecte directement le comportement en cas de charge maximale. Les canaux avec capacité BUFFERED(N) agissent comme un tampon de lissage : si le consommateur est temporairement plus lent que le producteur, les éléments s'accumulent. Si la vitesse moyenne du consommateur est constamment inférieure à celle du producteur, le tampon se remplit et la coroutine émettrice se suspend — c'est une contre-pression automatique qui protège contre la surcharge mémoire.

Pour la surveillance et le débogage de Channel, utilisez kotlinx-coroutines-debug : l'utilitaire affiche le nombre de coroutines actives, l'état de leurs canaux (ouvert/fermé, nombre d'éléments dans le tampon) et la pile d'appels des opérations send/receive suspendues. Channel peut également être enveloppé dans un proxy de journalisation : la classe LoggingChannel<T> délègue les appels au Channel réel, en journalisant les opérations send, receive et close. Cela aide à identifier les fuites de canal lorsque close() n'a pas été appelé et que la coroutine consommatrice attend éternellement de nouveaux éléments.

Questions fréquentes

En quoi Channel diffère-t-il de BlockingQueue ?

Channel utilise les fonctions suspend send() et receive() au lieu de put() et take() bloquants. Contrairement à BlockingQueue, Channel ne bloque pas le thread en cas de débordement — la coroutine se suspend, libérant le thread pour d'autres coroutines. C'est crucial pour une utilisation efficace des threads dans Kotlin.

Que se passe-t-il lors d'un send() sur un Channel fermé ?

Lors de l'appel de send() sur un canal fermé, une ClosedSendChannelException est levée. Avant d'envoyer, vérifiez isClosedForSend ou utilisez trySend() qui retourne false lors de la fermeture. close() garantit que les éléments déjà envoyés seront reçus avant que l'exception ne soit levée.

Quand utiliser Conflated Channel ?

Conflated Channel est utile pour les événements où seul le dernier état importe — barre de progression, position du curseur, coordonnées tactiles. Si le consommateur ne peut pas traiter tous les événements, les intermédiaires sont ignorés et le dernier est garanti d'être traité. Conflated Channel a capacity=-1.

Comment fermer un canal et traiter les éléments restants ?

Appelez channel.close() — le canal est marqué comme fermé pour l'envoi, mais les éléments déjà envoyés continuent d'être lus via receive(). L'itération avec for (item in channel) se termine automatiquement après la vidange du tampon. isClosedForSend retourne true immédiatement, isClosedForReceive retourne true après la vidange.

Peut-on remplacer Channel par Flow ?

Pas toujours. Flow est froid — une émission par collect. Si plusieurs producteurs indépendants écrivant dans un seul flux sont nécessaires, Channel est obligatoire. Pour un simple transfert de données entre deux coroutines, utilisez Channel. Pour les flux réactifs avec données, utilisez Flow.

Résumé

  • Channel est un primitif de synchronisation chaud pour transférer des données entre coroutines
  • Rendezvous — sans tampon, send bloque jusqu'à l'appel de receive
  • Buffered — avec tampon de capacité définie, send se suspend lorsqu'il est plein
  • Conflated — stocke uniquement la dernière valeur, les intermédiaires sont ignorés
  • Produce — constructeur de coroutines pour un canal avec fermeture automatique
  • Fan-out — plusieurs consommateurs distribuent les éléments en round-robin
  • Pour l'état de l'interface utilisateur, utilisez StateFlow, Channel est pour les files d'attente chaudes et la conversion de callbacks

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi