How do Channels enable fan-out and fan-in, and what ordering/fairness guarantees apply when multiple coroutines share one channel?
answer
- Fan-out: many workers read one channel, each element to one worker
- Fan-in: many producers send into one channel
- Fair = FIFO service of suspended send/receive
- Use bare for-loop per worker, not consumeEach
- No cross-worker completion ordering
basics
~10 sMany workers can read from one channel (fan-out) to split work, and many producers can write to one channel (fan-in) to merge results. The channel hands each value to exactly one receiver.
solid answer
~40 sBecause a Channel supports multiple senders and multiple receivers concurrently, you get fan-out by launching several worker coroutines that each iterate the same ReceiveChannel — each element is delivered to exactly one worker, so work is distributed without duplication. You get fan-in by having several producer coroutines send into one channel, merging their outputs. Delivery is fair: receivers and senders are served in first-come-first-served order, so no waiting coroutine is starved. There is no global ordering guarantee across workers — element order is preserved on the channel itself (FIFO), but once distributed to parallel workers, completion order is nondeterministic. Use a bare for-loop (not consumeEach) per worker so one worker finishing doesn't cancel the shared channel for the others. close() then lets every worker's loop end after the buffer drains.
code
kotlin · 5 linesval ch = Channel<Int>()
repeat(3) { id ->
launch { for (x in ch) println("w$id:$x") } // fan-out, bare for-loop
}
launch { for (i in 1..9) ch.send(i); ch.close() }go deeper
Understands one channel can have multiple readers/writers conceptually.
Sets up basic fan-out/fan-in and knows each element goes to one receiver.
Avoids the consumeEach pitfall, reasons about fairness and lack of cross-worker ordering.
Designs robust pipelines (backpressure, ordered reassembly, graceful shutdown) and knows when produce/actor or Flow fit better.
## Fan-out: one channel, many consumers Launch several worker coroutines that read the **same** `ReceiveChannel`. The channel delivers each element to exactly **one** receiver, so the workers naturally share the workload (a work-stealing queue). ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun CoroutineScope.produceNumbers() = produce { var n = 1 while (true) { send(n++); } } fun CoroutineScope.worker(id: Int, ch: ReceiveChannel<Int>) = launch { for (x in ch) println("worker $id got $x") // bare for-loop, not consumeEach } ``` Use a plain `for (x in ch)` per worker. If you used `consumeEach`, the **first** worker to finish would `cancel()` the shared channel out from under the others. ## Fan-in: many producers, one channel Several coroutines `send` into one channel; a single consumer merges them: ```kotlin suspend fun produceInto(ch: SendChannel<String>, msg: String, delayMs: Long) { while (true) { delay(delayMs); ch.send(msg) } } ``` ## Fairness Channel operations are **fair**: when multiple coroutines are suspended on `send`/`receive`, they are served in **first-in-first-out** order of arrival. No suspended sender or receiver is starved. (This is a documented guarantee, distinct from thread scheduling.) ## Ordering - The channel itself is **FIFO**: values come out in the order they were sent (for buffered/rendezvous). - Across **parallel workers**, there is **no** ordering guarantee on processing/completion — once an element is handed to a worker, that worker runs concurrently with the others. If you need ordered results, tag elements and reorder downstream, or fan results back into a single channel (fan-in) and sort. ## Lifecycle across multiple consumers With fan-out, call `close()` on the producing side once. Each worker's `for` loop ends after the channel is closed **and** drained. Because elements go to exactly one worker, total work is conserved — no element is processed twice or dropped. ## When to reach for produce/actor instead The `produce { }` builder gives a producer-owned `ReceiveChannel` that is auto-closed/cancelled with its scope, and `actor { }` serializes many senders into one state-owning consumer — both build on these same primitives (covered in the sibling leaf).
- Why not use consumeEach for fan-out workers?consumeEach cancels the shared channel when one worker's block completes, killing the other workers. Use a plain for-loop so each worker only stops at close.
- Is element processing order guaranteed across fan-out workers?No. The channel is FIFO on delivery, but workers run concurrently, so completion order is nondeterministic. Reorder downstream if you need order.
Fan-out is one conveyor belt feeding several packers; each box goes to whoever is free first.
saying these in an interview costs you the question
- Claiming each fan-out element is delivered to all receivers (it goes to exactly one)
- Using consumeEach in fan-out workers
- Assuming output order matches input order across parallel workers
- Thinking fairness is the same as OS thread scheduling
- Closing the channel per-worker instead of once on the producer