跪拜 Guibai
← Back to the summary

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

1. Core interface division of labour

SendChannel: Acts as the sending end of a channel, exposing only data-writing capability. Its core method is the suspending function send(element: E), which submits data to the channel. When the buffer is full, the current coroutine suspends automatically instead of blocking a thread; it also supports trySend for non-blocking send attempts and operations such as close() to shut the channel.

ReceiveChannel: Acts as the receiving end of a channel, exposing only data-reading capability. Its core method is the suspending function receive(): E, which takes data from the channel. When the channel is empty, the current coroutine suspends automatically and waits for new data to arrive; it also supports tryReceive for non-blocking receive attempts, and you can directly use a for-in loop to iterate over all elements in the channel until the channel is closed.

2. Usage example

fun main() = runBlocking {
    // Create a default unbuffered channel, holding both the send and receive ends
    val channel = Channel<Int>()
    // Extract the SendChannel side, usable only for sending data
    val sender: SendChannel<Int> = channel
    // Extract the ReceiveChannel side, usable only for receiving data
    val receiver: ReceiveChannel<Int> = channel

    // Producer coroutine: sends data via SendChannel
    launch {
        repeat(5) {
            sender.send(it)
            println("Sent: $it")
        }
        sender.close() // Close the channel after sending is complete
    }

    // Consumer coroutine: receives data via ReceiveChannel
    launch {
        // The for loop automatically iterates; the loop ends automatically after the channel closes
        for (element in receiver) {
            println("Received: $element")
        }
        println("Communication ended")
    }

    delay(1000)
}

3. Compiler-generated iteration logic

The logic actually generated by the compiler is equivalent to:

val iterator = receiver.iterator() // Obtain the iterator
while (iterator.hasNext()) {        // Check whether there is a next element
    val element = iterator.next()   // Get the next element
    println(element)
}

4. Channel implementation classes

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> {
    // The when expression here selects the concrete implementation class
    return when (capacity) {
        RENDEZVOUS -> RendezvousChannel(onUndeliveredElement) // Unbuffered, capacity 0
        CONFLATED -> ConflatedChannel(onUndeliveredElement)   // Conflated, capacity -1
        UNLIMITED -> LinkedListChannel(onUndeliveredElement)  // Unlimited buffer, uses a linked list
        else -> ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement) // Bounded buffer, uses an array
    }
}

The above implementation classes all inherit from a common base class, AbstractChannel.

// AbstractChannel.kt simplified source logic
abstract class AbstractChannel<E>(
    private val onUndeliveredElement: ((E) -> Unit)?
) : Channel<E>, SendChannel<E>, ReceiveChannel<E> {

    // 1. Provides the iterator entry point
    public final override fun iterator(): ChannelIterator<E> = Itr(this)

    // ... other send/receive logic
}

5. Iterator implementation principle

private class Itr<E>(@JvmField val channel: AbstractChannel<E>) : ChannelIterator<E> {
    var result: Any? = POLL_FAILED // E | POLL_FAILED | Closed

    override suspend fun hasNext(): Boolean {
        // check for repeated hasNext
        if (result !== POLL_FAILED) return hasNextResult(result)
        // fast path -- try poll non-blocking
        result = channel.pollInternal()
        if (result !== POLL_FAILED) return hasNextResult(result)
        // slow-path does suspend
        return hasNextSuspend()
    }

    private fun hasNextResult(result: Any?): Boolean {
        if (result is Closed<*>) {
            if (result.closeCause != null) throw recoverStackTrace(result.receiveException)
            return false
        }
        return true
    }

    private suspend fun hasNextSuspend(): Boolean = suspendCancellableCoroutineReusable sc@ { cont ->
        val receive = ReceiveHasNext(this, cont)
        while (true) {
            if (channel.enqueueReceive(receive)) {
                channel.removeReceiveOnCancel(cont, receive)
                return@sc
            }
            // hm... something is not right. try to poll
            val result = channel.pollInternal()
            this.result = result
            if (result is Closed<*>) {
                if (result.closeCause == null)
                    cont.resume(false)
                else
                    cont.resumeWithException(result.receiveException)
                return@sc
            }
            if (result !== POLL_FAILED) {
                @Suppress("UNCHECKED_CAST")
                cont.resume(true, channel.onUndeliveredElement?.bindCancellationFun(result as E, cont.context))
                return@sc
            }
        }
    }

    @Suppress("UNCHECKED_CAST")
    override fun next(): E {
        val result = this.result
        if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
        if (result !== POLL_FAILED) {
            this.result = POLL_FAILED
            return result as E
        }

        throw IllegalStateException("'hasNext' should be called prior to 'next' invocation")
    }
}

Inside pollInternal, this.result is assigned the following result:

val result = channel.pollInternal()
this.result = result

6. The pollInternal function

protected open fun pollInternal(): Any? {
    while (true) {
        val send = takeFirstSendOrPeekClosed() ?: return POLL_FAILED
        val token = send.tryResumeSend(null)
        if (token != null) {
            assert { token === RESUME_TOKEN }
            send.completeResumeSend()
            return send.pollResult
        }
        // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element
        send.undeliveredElement()
    }
}

protected fun takeFirstSendOrPeekClosed(): Send? =
    queue.removeFirstIfIsInstanceOfOrPeekIf<Send> { it is Closed<*> }

7. Internal queue mechanism

The design of Channel is inspired by Java's BlockingQueue, but it is purpose-built for non-blocking coroutine suspension.

Producer (SendChannel): When send(element) is called, the data element is actually wrapped into a node and placed at the tail of this internal queue. If the queue is full (for a bounded Channel), the sending coroutine is suspended until space becomes available.

Consumer (ReceiveChannel): When receive() is called, a data element is actually taken from the head of this internal queue. If the queue is empty, the receiving coroutine is suspended until new data enters the queue or the channel is closed.