Channel è un primitivo di sincronizzazione della libreria Kotlin Coroutines per trasferire dati tra coroutine. Secondo Kotlin Documentation, 2025, Channel implementa il pattern produttore-consumatore con invio bloccante attraverso funzioni suspend. Channel supporta le modalità Rendezvous, Buffered e Conflated, ciascuna delle quali definisce il comportamento in caso di overflow.
Punti Chiave
Channel è concettualmente simile a BlockingQueue di Java, ma con funzioni suspend send() e receive() invece di put() e take() bloccanti. Uno sviluppatore Kotlin usa Channel per organizzare lo scambio di dati tra coroutine senza sincronizzazione attraverso memoria condivisa. Il canale garantisce una consegna ordinata — l'ordine di invio corrisponde all'ordine di ricezione.
Per creare un Channel, viene chiamata la funzione factory Channel<T>(capacity). Il parametro capacity determina il tipo di canale: RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) o un numero specifico. Il tipo di elemento T viene specificato tramite generics. La chiusura del canale tramite close() segnala che non arriveranno nuovi elementi.
send(value) è una funzione suspend che sospende la coroutine mittente se il canale è pieno. receive() è una funzione suspend che sospende il destinatario se il canale è vuoto. Le alternative trySend() e tryReceive() sono versioni non bloccanti che restituiscono Boolean o null quando l'operazione non è possibile. Sono utili in contesti non suspend.
Kotlin fornisce quattro varianti di Channel attraverso la capacità del buffer: Rendezvous (capacità 0), Buffered (capacità N), Conflated (capacità 1, sovrascrittura) e Unlimited (capacità Int.MAX_VALUE). Ogni tipo risolve il proprio compito, dalla sincronizzazione rigorosa al buffering massivo di dati.
Rendezvous Channel è il più rigoroso: send() si blocca fino a quando receive() non viene chiamato in un'altra coroutine. In sostanza, è un punto di rendez-vous di due coroutine. Ideale per handshake rigoroso quando il mittente deve attendere che il destinatario elabori l'elemento. La perdita di dati è esclusa — send non viene completato fino a quando receive non viene eseguito.
Conflated Channel memorizza solo l'ultimo valore inviato. Se il mittente inserisce un nuovo elemento prima che il destinatario raccolga quello vecchio, quello vecchio viene scartato. Conflated Channel è utile per lo stato dell'interfaccia utente: se un utente cambia rapidamente il cursore, i valori intermedi possono essere scartati e solo l'ultimo elaborato.
Il classico pattern Produttore-Consumatore su Channel è implementato attraverso coroutine parallele. Produttore chiama send(value) in un ciclo, consumatore chiama receive(value). Il produttore e il consumatore possono lavorare su Dispatchers diversi: produttore su Dispatchers.IO, consumatore su Dispatchers.Main. Channel sincronizza automaticamente l'accesso senza Lock o synchronized.
Fan-out — più consumatori su un singolo canale. Ogni elemento va esattamente a un consumatore (distribuzione round-robin). Fan-in — più produttori scrivono in un singolo canale. Le coroutine mittenti competono per l'invio, ma l'ordine degli elementi viene preservato. Entrambi gli scenari non richiedono sincronizzazione aggiuntiva.
Produce è un costruttore di coroutine che crea un canale con chiusura automatica. La funzione produce { } restituisce un ReceiveChannel — un canale di sola lettura per il consumatore. All'interno del costruttore, send() invia dati, e quando il blocco si completa o si verifica un'eccezione, il canale si chiude automaticamente, prevenendo perdite.
La libreria kotlinx.coroutines fornisce select — un'espressione che attende il primo canale completato tra diverse alternative. Select permette di multiplexare più canali: ad esempio, attendere dati da due fonti e processare quella che ha risposto per prima. Sintassi — select<T> { channel1.onReceive { } channel2.onReceive { } }. Questa è un'alternativa all'operatore amb in Rx.
Il primo esempio è un semplice Rendezvous Channel dove il mittente attende la ricezione:
val channel = Channel<String>()
scope.launch {
channel.send("Hello")
println("Inviato")
}
scope.launch {
val msg = channel.receive()
println("Ricevuto: $msg")
}
Il secondo esempio — più consumatori su un singolo canale (fan-out):
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")
}
}
}
Il terzo esempio — utilizzo del costruttore produce con gestione degli errori:
val source = produce {
for (i in 1..5) {
delay(200)
send(i)
}
}
scope.launch {
source
.consumeAsFlow()
.catch { println("Errore: $it") }
.collect { println("Elemento: $it") }
}
Channel è un primitivo caldo: i dati vengono emessi indipendentemente dagli abbonati. Flow è freddo: i dati vengono generati all'abbonamento. Channel supporta più produttori e consumatori con consegna garantita di ogni elemento a un consumatore (fan-out). Flow non è progettato per più produttori indipendenti.
Channel utilizza un buffer con capacità configurabile e funzioni suspend send/receive per la gestione della contropressione. Flow utilizza il meccanismo suspend collect con contropressione automatica attraverso le coroutine. Channel è uno strumento di basso livello per scenari specifici: conversione di callback, modello attore, coda di attività con più mittenti.
Per scenari quotidiani in Android (stato dell'interfaccia utente, stream reattivi dal database), Google raccomanda Flow invece di Channel. Channel dovrebbe essere usato quando è necessario uno scambio di dati caldo tra coroutine con controllo preciso del buffer, o quando si convertono interfacce callback tramite callbackFlow, la cui implementazione interna utilizza Channel.
Un esempio pratico importante: nell'implementazione di un client WebSocket, Channel permette di scrivere messaggi da una coroutine e leggere da un'altra con la garanzia che ogni messaggio venga elaborato esattamente una volta. Flow non è adatto per questo compito perché è freddo e non supporta più produttori. Channel con capacità UNLIMITED garantisce che i messaggi in arrivo non vengano persi durante ritardi temporanei del consumatore.
La gestione del ciclo di vita del canale è una parte importante del lavoro con Channel. Il canale deve essere chiuso quando tutti i dati sono stati inviati in modo che il consumatore possa completare l'iterazione. Chiamare channel.close() segnala che non arriveranno nuovi elementi. Il consumatore può iterare tramite for (item in channel) — il ciclo terminerà automaticamente dopo close() e lo svuotamento del buffer. In alternativa, il consumatore può chiamare receive() in un ciclo con gestione di ClosedReceiveChannelException.
Channel è attivamente utilizzato in Android per implementare EventBus senza dipendenze: un Channel<Event> globale con strategia Broadcast permette di inviare eventi da qualsiasi punto dell'applicazione. A differenza dei bus basati su LiveData, Channel non è legato al ciclo di vita e non richiede pulizia durante la transizione tra schermate. send() dal ViewModel e receive() in Activity/Fragment tramite lifecycleScope forniscono comunicazione type-safe senza classi Event. Molteplici consumatori su un Channel distribuiscono il carico — ogni elemento viene elaborato una volta, prevenendo l'elaborazione duplicata di un singolo evento in diversi abbonati.
Nei sistemi ad attori, Channel funge da base per implementare un mailbox — una coda di messaggi per l'attore. Un attore è una coroutine che legge messaggi da un Channel in un ciclo e li elabora sequenzialmente. Questo approccio garantisce che ogni messaggio venga elaborato nell'ordine di invio, senza race condition. Kotlin non ha un attore incorporato come tipo (a differenza di Akka), ma Channel + launch è un sostituto leggero.
Per lo scambio bidirezionale vengono utilizzate coppie di canali: un canale per le richieste dal client al server, il secondo per le risposte dal server al client. Ad esempio, nell'implementazione di una Pipe in un'applicazione multithread: il produttore scrive in OutputChannel, il consumatore legge da InputChannel. Le funzioni suspend send e receive garantiscono che il Produttore-Consumatore non sovraccarichi lo stack di chiamate, poiché le coroutine si sospendono invece di bloccarsi. Channel con capacità BUFFERED è adatto per la maggior parte degli scenari in cui la velocità del produttore e del consumatore sono approssimativamente uguali. Per scenari asimmetrici, utilizzare UNLIMITED in modo che il produttore non si sospenda quando il consumatore è occupato — questo riduce il rischio di deadlock ma aumenta il consumo di memoria.
Quando si progetta un'architettura con canali, è importante ricordare capacity: la scelta della capacità influisce direttamente sul comportamento sotto carico di picco. I canali con capacità BUFFERED(N) agiscono come un buffer di livellamento: se il consumatore è temporaneamente più lento del produttore, gli elementi si accumulano. Se la velocità media del consumatore è costantemente inferiore a quella del produttore, il buffer si riempirà e la coroutine mittente verrà sospesa — questa è una contropressione automatica che protegge dal sovraccarico di memoria.
Per il monitoraggio e il debugging di Channel, utilizzare kotlinx-coroutines-debug: l'utilità mostra il numero di coroutine attive, lo stato dei loro canali (aperto/chiuso, numero di elementi nel buffer) e lo stack di chiamate delle operazioni send/receive sospese. Channel può anche essere avvolto in un proxy di registrazione: la classe LoggingChannel<T> delega le chiamate al Channel reale, registrando le operazioni send, receive e close. Questo aiuta a identificare perdite di canale quando close() non è stato chiamato e la coroutine consumatrice aspetta per sempre nuovi elementi.
Domande Frequenti
Channel utilizza funzioni suspend send() e receive() invece di put() e take() bloccanti. A differenza di BlockingQueue, Channel non blocca il thread in caso di overflow — la coroutine si sospende, liberando il thread per altre coroutine. Questo è fondamentale per un uso efficiente dei thread in Kotlin.
Quando si chiama send() su un canale chiuso, viene lanciata ClosedSendChannelException. Prima di inviare, verificare isClosedForSend o utilizzare trySend() che restituisce false quando è chiuso. close() garantisce che gli elementi già inviati vengano ricevuti prima che l'eccezione venga lanciata.
Conflated Channel è utile per eventi in cui solo l'ultimo stato è importante — barra di avanzamento, posizione del cursore, coordinate tattili. Se il consumatore non può elaborare tutti gli eventi, quelli intermedi vengono scartati e l'ultimo viene garantitamente elaborato. Conflated Channel ha capacity=-1.
Chiamare channel.close() — il canale viene contrassegnato come chiuso per l'invio, ma gli elementi già inviati continuano a essere letti tramite receive(). L'iterazione con for (item in channel) termina automaticamente dopo lo svuotamento del buffer. isClosedForSend restituisce true immediatamente, isClosedForReceive restituisce true dopo lo svuotamento.
Non sempre. Flow è freddo — una emissione per ogni collect. Se sono necessari più produttori indipendenti che scrivono in un singolo stream, Channel è obbligatorio. Per un semplice trasferimento di dati tra due coroutine, utilizzare Channel. Per stream reattivi con dati, utilizzare Flow.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche