跪拜 Guibai
← All articles
Android

Kotlin Channels Split Send and Receive Into Separate Interfaces — Here’s the Iterator That Powers the Loop

By hai_android ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Separating send and receive into distinct interfaces makes it harder to accidentally consume from a producer or write from a consumer — a compile-time guard that matters once channels are passed across module boundaries. Understanding the iterator’s fast-poll-then-suspend path also explains why a for-in loop over a channel never burns CPU spinning on an empty queue.

Summary

A Kotlin Channel is really two interfaces: SendChannel for writing and ReceiveChannel for reading. The factory function picks one of four internal implementations — rendezvous, conflated, unlimited, or array-backed — all inheriting from AbstractChannel, which implements both interfaces and provides the iterator that makes `for (element in receiver)` work. That iterator, `Itr`, first tries a non-blocking poll via `pollInternal`; if the queue is empty, it falls back to a suspending path that enqueues a receive node and waits.

The internal queue is modeled on Java’s BlockingQueue but designed for coroutine suspension instead of thread blocking. Producers wrap elements into nodes and append them to the tail; consumers pull from the head. When the buffer is full or empty, the affected coroutine suspends rather than tying up a thread, and the channel’s close mechanism propagates a `Closed` sentinel through the queue to terminate iteration cleanly.

A working example shows a producer coroutine sending five integers through a sender reference and then closing the channel, while a consumer coroutine iterates with a plain for-loop that the compiler desugars into `hasNext()`/`next()` calls on the channel’s iterator.

Takeaways
SendChannel<E> exposes only `send` (suspending) and `trySend` (non-blocking), plus `close`; ReceiveChannel<E> exposes only `receive` (suspending), `tryReceive`, and iteration support.
A `for (element in receiver)` loop compiles down to an iterator whose `hasNext()` first does a non-blocking poll, then suspends only if the queue is empty.
The Channel factory selects among four backing stores: RendezvousChannel (capacity 0), ConflatedChannel (capacity -1, merging latest), LinkedListChannel (unlimited), and ArrayChannel (bounded array).
All implementations extend AbstractChannel, which implements both SendChannel and ReceiveChannel and supplies the shared iterator.
The internal queue is a coroutine-aware analogue of BlockingQueue: producers enqueue at the tail, consumers dequeue from the head, and full/empty conditions suspend the calling coroutine instead of blocking a thread.
Channel closure injects a `Closed` sentinel into the queue; the iterator sees it, stops iteration, and optionally throws if the close cause was an exception.
Conclusions

Exposing SendChannel and ReceiveChannel as separate types is a capability-restriction pattern that Kotlin’s standard library bakes in at the type level — the same Channel instance satisfies both, but you choose which reference to pass to each coroutine.

The iterator’s two-phase `hasNext` (fast non-blocking poll, then slow suspend) is a practical optimisation that avoids suspension overhead when elements are already buffered, which matters for high-throughput pipelines.

ConflatedChannel’s capacity of -1 is a sentinel value, not a literal size; it signals merge-on-write semantics rather than a fixed slot count, which is easy to misread when scanning the Channel factory source.

Concepts & terms
RendezvousChannel
An unbuffered channel (capacity 0) where send suspends until a receiver is ready, and receive suspends until a sender is ready — a direct hand-off with no intermediate storage.
ConflatedChannel
A channel that keeps only the latest value; new sends overwrite any unread element, and receivers always get the most recent item. Capacity is represented as -1 in the factory.
POLL_FAILED
A sentinel object returned by `pollInternal` when the queue is empty and no element could be retrieved without suspending.
Closed sentinel
A special node injected into the channel’s internal queue when `close()` is called; the iterator and receive operations detect it to terminate iteration or throw a close-cause exception.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗