Channel — je synchronizační primitiv z knihovny Kotlin Coroutines pro přenos dat mezi korutinami. Podle Kotlin Documentation, 2025, Channel implementuje vzor producer-consumer s blokujícím odesíláním přes suspend-funkce. Channel podporuje režimy Rendezvous, Buffered a Conflated, z nichž každý určuje chování při přetečení.
Hlavní
Channel — je koncepčně podobný BlockingQueue z Javy, ale se suspend-funkcemi send() a receive() místo blokujících put() a take(). Vývojář Kotlinu používá Channel k organizaci výměny dat mezi korutinami bez synchronizace prostřednictvím sdílené paměti. Kanál garantuje uspořádané doručení — pořadí odesílání se shoduje s pořadím přijímání.
Pro vytvoření Channel se volá tovární funkce Channel<T>(capacity). Parametr capacity určuje typ kanálu: RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) nebo konkrétní číslo. Typ prvku T se nastavuje generikem. Uzavření kanálu pomocí close() signalizuje, že nebudou žádné nové prvky.
send(value) — suspend-funkce, která pozastaví odesílající korutinu, pokud je kanál plný. receive() — suspend-funkce, která pozastaví příjemce, pokud je kanál prázdný. Alternativy trySend() a tryReceive() — neblokující verze, které vracejí Boolean nebo null při nemožnosti operace. Jsou užitečné v nesuspend kontextech.
Kotlin poskytuje čtyři varianty Channel prostřednictvím kapacity bufferu: Rendezvous (kapacita 0), Buffered (kapacita N), Conflated (kapacita 1, přepis) a Unlimited (kapacita Int.MAX_VALUE). Každý typ řeší svůj úkol, od přísné synchronizace až po hromadné bufferování dat.
Rendezvous Channel — nejpřísnější: send() se blokuje, dokud se nezavolá receive() v jiné korutině. V podstatě jde o místo setkání dvou korutin. Ideální pro přísný handshake, když odesílatel musí počkat, až příjemce zpracuje prvek. Ztráta dat je vyloučena — send se nedokončí, dokud receive není provedeno.
Conflated Channel — ukládá pouze poslední odeslanou hodnotu. Pokud odesílatel vložil nový prvek dříve, než příjemce vzal starý, starý se zahazuje. Conflated Channel je užitečný pro stav UI: pokud uživatel rychle mění jezdec, průběžné hodnoty lze zahodit a zpracovat pouze poslední.
Klasický vzor Producer-Consumer na Channel se implementuje pomocí paralelních korutin. Producer ve smyčce volá send(value), consumer — receive(value). Výrobce a spotřebitel mohou pracovat na různých Dispatchers: producer na Dispatchers.IO, consumer na Dispatchers.Main. Channel automaticky synchronizuje přístup bez Lock a synchronized.
Fan-out — několik spotřebitelů na jednom kanále. Každý prvek se dostane přesně k jednomu spotřebiteli (round-robin rozdělení). Fan-in — několik výrobců zapisuje do jednoho kanálu. Odesílající korutiny soutěží o odeslání, ale pořadí prvků je zachováno. Oba scénáře nevyžadují další synchronizaci.
Produce — je korutinový builder, který vytváří kanál s automatickým uzavíráním. Funkce produce { } vrací ReceiveChannel — read-only kanál pro spotřebitele. Uvnitř builderu send() odesílá data a při dokončení bloku nebo výjimce se kanál automaticky uzavře, čímž se předchází únikům.
Knihovna kotlinx.coroutines poskytuje select — výraz, který čeká na první dokončený kanál z několika alternativ. Select umožňuje multiplexovat více kanálů: například čekat na data ze dvou zdrojů a zpracovat ten, který odpověděl první. Syntaxe — select<T> { channel1.onReceive { } channel2.onReceive { } }. Toto je alternativa k operátoru amb v Rx.
První příklad — nejjednodušší Rendezvous Channel, kde odesílatel čeká na příjem:
val channel = Channel<String>()
scope.launch {
channel.send("Hello")
println("Odesláno")
}
scope.launch {
val msg = channel.receive()
println("Přijato: $msg")
}
Druhý příklad — několik spotřebitelů na jednom kanále (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("Spotřebitel #$id: $msg")
}
}
}
Třetí příklad — použití builderu produce s ošetřením chyb:
val source = produce {
for (i in 1..5) {
delay(200)
send(i)
}
}
scope.launch {
source
.consumeAsFlow()
.catch { println("Chyba: $it") }
.collect { println("Prvek: $it") }
}
Channel — je hot primitiv: data jsou emitována nezávisle na odběratelech. Flow — cold: data jsou generována při přihlášení k odběru. Channel podporuje několik výrobců a spotřebitelů se zaručeným doručením každého prvku jednomu spotřebiteli (fan-out). Flow není určen pro několik nezávislých výrobců.
Channel používá buffer s nastavitelnou kapacitou a suspend-funkce send/receive pro řízení backpressure. Flow používá suspend mechanismus collect s automatickým backpressure pomocí korutin. Channel — nízkoúrovňový nástroj pro specifické scénáře: konverze callbacků, actor model, fronta úkolů s několika odesílateli.
Pro každodenní scénáře v Androidu (stav UI, reaktivní streamy z databáze) Google doporučuje Flow, nikoli Channel. Channel by měl být použit, když je potřeba hot výměna dat mezi korutinami s přesnou kontrolou bufferování, nebo při konverzi callback rozhraní prostřednictvím callbackFlow, jehož interní implementace používá Channel.
Důležitý praktický příklad: při implementaci WebSocket klienta Channel umožňuje psát zprávy z jedné korutiny a číst z druhé se zárukou, že každá zpráva bude zpracována právě jednou. Flow pro tento úkol není vhodný, protože je cold a nepodporuje několik výrobců. Channel s kapacitou UNLIMITED zajišťuje, že příchozí zprávy nejsou ztraceny při dočasných zpožděních spotřebitele.
Řízení životního cyklu kanálu — důležitá součást práce s Channel. Kanál musí být uzavřen, když jsou všechna data odeslána, aby spotřebitel mohl dokončit iteraci. Volání channel.close() signalizuje, že nebudou žádné nové prvky. Spotřebitel může iterovat pomocí for (item in channel) — smyčka se automaticky ukončí po close() a vyprázdnění bufferu. Alternativně může spotřebitel volat receive() ve smyčce s ošetřením ClosedReceiveChannelException.
Channel je aktivně používán v Androidu pro implementaci EventBus bez závislostí: globální Channel<Event> se strategií Broadcast umožňuje odesílat události z libovolného místa aplikace. Na rozdíl od sběrnice založené na LiveData, Channel není vázán na lifecycle a nevyžaduje nulování při přechodu mezi obrazovkami. send() z ViewModel a receive() v Activity/Fragment přes lifecycleScope zajišťují typově bezpečnou komunikaci bez Event tříd. Několik spotřebitelů na Channel rozděluje zátěž — každý prvek je zpracován jednou, což zabraňuje duplikaci zpracování stejné události v různých odběratelech.
V actor systémech slouží Channel jako základ pro implementaci mailbox — fronty zpráv pro actor. Actor — je korutina, která ve smyčce čte zprávy z Channel a zpracovává je sekvenčně. Tento přístup garantuje, že každá zpráva je zpracována v pořadí odeslání, bez datových závodů. Kotlin nemá vestavěný actor jako typ (na rozdíl od Akka), ale Channel + launch je lehká náhrada.
Pro obousměrnou výměnu se používají dvojice kanálů: jeden kanál pro požadavky od klienta k serveru, druhý — pro odpovědi od serveru ke klientovi. Například při implementaci Pipe ve vícevláknové aplikaci: výrobce zapisuje do OutputChannel, spotřebitel čte z InputChannel. Suspend-funkce send a receive garantují, že Producer-Consumer nepřetečí zásobník volání, protože korutiny jsou pozastavovány, nikoli blokovány. Channel s capacity BUFFERED je vhodný pro většinu scénářů, kde je rychlost výrobce a spotřebitele přibližně stejná. Pro asymetrické scénáře použijte UNLIMITED, aby výrobce nebyl pozastaven, když je spotřebitel zaneprázdněn — to snižuje riziko deadlocku, ale zvyšuje spotřebu paměti.
Při navrhování architektury na kanálech je důležité pamatovat na capacity: výběr kapacity přímo ovlivňuje chování při špičkovém zatížení. Kanály s kapacitou BUFFERED(N) fungují jako vyhlazovací buffer: pokud je spotřebitel dočasně pomalejší než výrobce, prvky se hromadí. Pokud je průměrná rychlost spotřebitele stabilně nižší než výrobce, buffer se zaplní a odesílající korutina bude pozastavena — to je automatický backpressure, který chraní před přetížením paměti.
Pro monitorování a ladění Channel použijte kotlinx-coroutines-debug: nástroj ukazuje počet aktivních korutin, stav jejich kanálů (otevřen/uzavřen, počet prvků v bufferu) a zásobník volání pozastavených send/receive operací. Channel lze také zabalit do protokolujícího proxy: třída LoggingChannel<T> deleguje volání na skutečný Channel a loguje send, receive a close operace. To pomáhá odhalit úniky kanálů, když close() nebyl zavolán a korutina spotřebitele věčně čeká na nové prvky.
Často kladené dotazy
Channel používá suspend-funkce send() a receive() místo blokujících put() a take(). Na rozdíl od BlockingQueue, Channel neblokuje vlákno při přetečení — korutina je pozastavena, čímž uvolňuje vlákno pro jiné korutiny. To je kritické pro efektivní využití vláken v Kotlinu.
Při volání send() na uzavřeném kanálu je vyhozena ClosedSendChannelException. Před odesláním zkontrolujte isClosedForSend nebo použijte trySend(), který při uzavření vrací false. close() garantuje, že již odeslané prvky budou přijaty před vyhozením výjimky.
Conflated Channel je užitečný pro události, kde záleží pouze na posledním stavu — ukazatel průběhu, pozice jezdce, souřadnice dotyku. Pokud spotřebitel nestíhá zpracovat všechny události, průběžné se zahazují a poslední je garantovaně zpracován. Conflated Channel má capacity=-1.
Zavolejte channel.close() — kanál je označen jako uzavřený pro odesílání, ale již odeslané prvky jsou nadále čteny pomocí receive(). Iterace v for (item in channel) se automaticky ukončí po vyprázdnění bufferu. isClosedForSend vrací true okamžitě, isClosedForReceive — po vyprázdnění.
Ne vždy. Flow je cold — jedna emise na jeden collect. Pokud je potřeba několik nezávislých výrobců zapisujících do jednoho streamu, Channel je nezbytný. Pro jednoduchý přenos dat mezi dvěma korutinami použijte Channel. Pro reaktivní streamy s daty — Flow.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také