skip to content

Synchronization Primitives

The suspending equivalents of classic concurrency tools: mutexes and semaphores that suspend rather than block, channels for passing values between coroutines, select for racing, and the producer/actor builders. They come up whenever shared state enters the conversation.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What is a Channel<T> in Kotlin coroutines, and how do producer and consumer coroutines communicate through it?

level: juniorimportance: must knowfreq 70%

answer

  1. send/receive are suspend
  2. hot, not cold like Flow
  3. RENDEZVOUS = no buffer, direct handover
  4. for (x in channel) iterates receiver
  5. close() ends the loop

basics

~10 s

A Channel is a pipe between coroutines. One coroutine puts values in with send, another takes them out with receive. Both suspend (wait politely) until the other side is ready.

solid answer

~40 s

Channel<T> is a coroutine-safe queue used to pass a stream of values between coroutines. A producer calls suspend fun send(value), a consumer calls suspend fun receive(). Both are suspending functions: with the default RENDEZVOUS capacity, send suspends until a receiver is ready and receive suspends until a value is available, so the channel hands values over directly with no buffering. Unlike Flow (cold), a Channel is hot: values flow as soon as both ends meet, regardless of whether anyone is collecting yet. You create one with Channel<Int>() and typically iterate the consumer side with for (x in channel) or channel.consumeEach { }. When the producer is done it calls close(), which lets the consumer loop end cleanly.

code

kotlin · 6 lines
kotlin
val channel = Channel<Int>()
launch {
    for (x in 1..3) channel.send(x)
    channel.close()
}
for (y in channel) println(y)  // 1 2 3

go deeper

for a junior

Knows send/receive exist, that they suspend, and that close ends the loop.

for a middle

Explains hot vs cold vs Flow and the rendezvous handover precisely.

for a senior

Discusses when a Channel is the right tool vs a Flow and the resource/ownership implications.

for a principal

Frames Channel as the low-level communication primitive underlying produce/actor and shared-flow design tradeoffs.

## What a Channel is A `Channel<T>` is a concurrency primitive in `kotlinx.coroutines` for passing a *stream* of values of type `T` between coroutines. Think of it as a coroutine-aware `BlockingQueue`: instead of blocking a thread, its operations **suspend** the calling coroutine, freeing the thread to do other work. ## The two core operations - `suspend fun send(element: T)` — a **producer** offers a value into the channel. - `suspend fun receive(): T` — a **consumer** takes the next value out. Both are `suspend` functions, so they can only be called from a coroutine or another suspend function. "Suspend" means the coroutine pauses without blocking the underlying thread and resumes later when the operation can proceed. ## Hot vs cold A Channel is a **hot** stream: it exists and conducts values independently of consumers. This contrasts with `Flow`, which is **cold** (the producing code reruns for each collector). A Channel models *communication between coroutines*; a Flow models a *cold declarative pipeline*. ## Rendezvous by default With no capacity argument, `Channel<T>()` uses `RENDEZVOUS`: there is no buffer. `send` suspends until some coroutine calls `receive`, and vice versa. The value is handed over directly — a meeting point ("rendezvous"). ## Iterating the consumer side The channel implements `ReceiveChannel<T>`, which is iterable in a coroutine: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun main() = runBlocking { val channel = Channel<Int>() launch { // producer coroutine for (x in 1..3) channel.send(x * x) channel.close() // signal: no more values } for (y in channel) { // suspends until each value / close println(y) // 1, 4, 9 } } ``` The `for (y in channel)` loop calls `receive` under the hood and ends when the channel is **closed and drained**. ## Closing `close()` marks the channel as done. Pending buffered values are still delivered; afterwards the consumer loop terminates. Calling `send` on a closed channel throws `ClosedSendChannelException`. ## Why use one Channels are the building block for producer/consumer pipelines, fan-out/fan-in work distribution, and the higher-level `produce { }` and `actor { }` builders.

  • How is a Channel different from a Flow?
    Channel is hot and is a communication primitive between coroutines; Flow is cold and reruns its producer per collector. Channels have explicit send/receive; Flow has emit/collect.
  • What happens to receive when the channel is closed and empty?
    receive() throws ClosedReceiveChannelException; the for-loop and consumeEach handle this for you and just exit.

Like passing a baton in a relay: the runner (sender) waits with the baton until the next runner (receiver) is there to take it.

saying these in an interview costs you the question

  • Saying a Channel is cold or reruns per consumer
  • Claiming send/receive block the thread instead of suspending
  • Thinking you can call send/receive from a normal (non-suspend) function
  • Forgetting to close() and wondering why the consumer loop hangs forever

context

open as a page

Why shouldn't you use the JVM `synchronized` block (or a `ReentrantLock`) to protect shared state inside a coroutine, and what does Kotlin offer instead?

level: juniorimportance: must knowfreq 70%

basics

~10 s

synchronized blocks the whole thread while it waits. Coroutines share threads, so blocking one stalls others. Kotlin's Mutex instead suspends the coroutine, freeing the thread for other work.

open as a page

Explain the Channel capacity options RENDEZVOUS, BUFFERED, CONFLATED, and UNLIMITED, and how each affects send.

level: middleimportance: must knowfreq 60%

basics

~10 s

Capacity controls how many values a channel holds before send has to wait. Rendezvous holds none, buffered holds a few, unlimited holds everything, and conflated keeps only the latest and never waits.

open as a page

Explain what `Mutex.withLock { }` does mechanically, including how it behaves on exceptions and cancellation. How does it differ from manual `lock()`/`unlock()`?

level: middleimportance: must knowfreq 60%

basics

~10 s

withLock acquires the lock, runs your block, and always releases it afterward — even if the block throws or the coroutine is cancelled. Manual lock()/unlock() makes you remember to release it yourself.

open as a page

Why does `produce` return a `ReceiveChannel` while `actor` returns a `SendChannel`, and what are the receiver scopes inside each?

level: middleimportance: must knowfreq 35%

basics

~20 s

With produce the coroutine makes values for you, so you get the read end (ReceiveChannel). With actor you feed it messages, so you get the write end (SendChannel). Each is the opposite end of the channel from what the coroutine uses.

open as a page

What does the `produce` coroutine builder do, and what does it return?

level: juniorimportance: should knowfreq 45%

basics

~10 s

produce starts a coroutine that streams out values. It gives you back a channel you can read from to receive each value the coroutine sends.

open as a page

What is the `select` expression in Kotlin coroutines, and what problem does it solve?

level: juniorimportance: should knowfreq 45%

basics

~10 s

select lets a coroutine wait on several suspending operations at once and proceed with whichever finishes first, ignoring the rest. It is like racing multiple options and taking the winner.

open as a page

How do you correctly close a Channel and iterate it, and what is the difference between for-in, consumeEach, and consume?

level: middleimportance: should knowfreq 50%

basics

~10 s

The producer calls close() when done. The consumer reads with a for-in loop or consumeEach. consumeEach also cancels the channel when finished, so it cleans up even if something goes wrong.

open as a page

You need to call a flaky external API but must never have more than 5 in-flight requests at once across many coroutines. How do you implement this with coroutine sync primitives?

level: middleimportance: should knowfreq 55%

basics

~10 s

Create one shared Semaphore(5) and wrap each call in semaphore.withPermit { ... }. Coroutines that arrive when all 5 permits are taken suspend until a permit is freed.

open as a page

How does the `actor` builder let you safely mutate shared state without a Mutex?

level: middleimportance: should knowfreq 40%

basics

~20 s

actor runs a single coroutine that owns the state and processes messages one at a time from its inbox. Because only that one coroutine touches the state, there is no race, so you don't need a lock.

open as a page

How do you race two suspending operations and take the faster result using `onAwait` in `select`?

level: middleimportance: should knowfreq 38%

basics

~10 s

Start each operation with async to get a Deferred, then use select with each deferred's onAwait clause. select resumes with whichever finishes first. Remember to cancel the loser.

open as a page

How do `onReceive` and `onSend` work inside `select`, and when would you use each?

level: middleimportance: should knowfreq 40%

basics

~10 s

onReceive waits until a channel has a value to read; onSend waits until a channel can accept a value. Inside select you can wait on several channels and act on whichever is ready first.

open as a page

How do Channels enable fan-out and fan-in, and what ordering/fairness guarantees apply when multiple coroutines share one channel?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Many 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.

open as a page

When would you choose a `Mutex` over alternatives like a confined single-threaded dispatcher, atomics, or `StateFlow.update`? Discuss the tradeoffs for protecting shared mutable state in coroutines.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use a Mutex when several coroutines must take turns mutating shared state and the critical section is small. Prefer atomics for simple counters, StateFlow.update for observable state, or confining work to one thread to avoid locks entirely.

open as a page

A teammate's code calls a `suspend` function while holding a `Mutex`, and that function in turn calls `withLock` on the same mutex. It hangs forever. Explain why, and how you'd fix or avoid it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Kotlin's Mutex is not reentrant. A coroutine that already holds the lock can't take it again — it suspends waiting for itself and never resumes. Fix by not re-locking: split the locked and unlocked parts.

open as a page

When consuming a `produce` channel, why is `consume`/`consumeEach` recommended, and what cleanup do they guarantee?

level: seniorimportance: should knowfreq 28%

basics

~20 s

If you stop reading early without cleanup, the producer coroutine can hang waiting forever. consume and consumeEach cancel the channel when you're done or if you break out, which stops the producer and frees resources.

open as a page

What does `onTimeout` do in a `select` expression, and how does it differ from wrapping the `select` in `withTimeout`?

level: seniorimportance: should knowfreq 30%

basics

~10 s

onTimeout(duration) is a clause that wins if no other clause becomes ready within the time. It lets select fall back gracefully. Unlike withTimeout, it returns a value instead of throwing or cancelling.

open as a page

What are trySend, tryReceive, and receiveCatching on a Channel, and when would you use them instead of send/receive?

level: seniorimportance: nice to knowfreq 28%

basics

~10 s

trySend and tryReceive are non-suspending attempts: they try once and tell you whether it worked, without waiting. receiveCatching waits but returns a result object instead of throwing when the channel is closed.

open as a page

Explain `select`'s clause-ordering bias and the resulting fairness/starvation concerns. How do you mitigate them?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

select checks clauses top-to-bottom, so if several are ready it always picks the first listed. In a busy loop that can starve later clauses. Rotate or randomize clause order to keep things fair.

open as a page

Both `produce` and `actor` are flagged experimental/obsolete. As a principal engineer, when would you still use them, and what modern alternatives would you reach for instead?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

produce is good for hot, channel-based streaming pipelines; for most output streams prefer Flow. actor serializes state via messages, but today a Mutex or a StateFlow is usually simpler and stable. Use the builders when the channel semantics genuinely fit.

open as a page