Channels, shared state and advanced concurrency
Channel and produce, select, Mutex and atomics, SharingStarted strategies, exception handling, and deterministic coroutine tests.
Channels and pipelines
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
for (i in 1..5) send(i)
} // the channel closes automatically when the block completes
suspend fun main() = coroutineScope {
val input = numbers()
val doubled = produce {
for (value in input) send(value * 2)
}
for (result in doubled) println(result)
// a rendezvous channel: send suspends until a receiver takes the value
val handoff = Channel<String>(Channel.RENDEZVOUS)
launch { handoff.send("ready") }
println(handoff.receive())
// buffered capacity trades memory for throughput
val queue = Channel<Int>(capacity = 64)
queue.close() // close lets receivers finish normally
}produceis a scope-bound coroutine that owns a channel and cancels it if the scope dies.Channel.CONFLATEDkeeps only the newest value — ideal for progress updates where intermediate values do not matter.- Iterating a channel ends when it is closed. Forgetting to close leaves consumers suspended forever.
- Channels are for hand-off between coroutines;
Flowis for a cold stream that can be collected many times.
Mutex, atomics and state flows
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.atomic.AtomicLong
class Counter {
private val mutex = Mutex()
private var total = 0L
suspend fun add(amount: Long) = mutex.withLock {
total += amount
total
}
// for a single value, an atomic is enough and never suspends
private val hits = AtomicLong()
fun hit() = hits.incrementAndGet()
}
class Ticker(private val repository: Repository) {
private val _state = MutableStateFlow(State())
val state: StateFlow<State> = _state.asStateFlow()
val live: StateFlow<State> = _state
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = State()
)
suspend fun refresh() {
_state.update { it.copy(loading = true) }
val result = runCatching { repository.load() }
_state.update { current ->
result.fold(
onSuccess = { current.copy(loading = false, items = it) },
onFailure = { current.copy(loading = false, error = it.message) }
)
}
}
}| Tool | Use it for | Not for |
|---|---|---|
Mutex | Guarding a critical section | Long blocking work |
AtomicLong | A single counter or flag | Compound multi-field updates |
StateFlow | Observable state with a current value | One-off events |
SharedFlow | Broadcast events to many collectors | Holding state |
Channel | A queue for exactly one consumer | Sharing state |
Exceptions and deterministic tests
import kotlinx.coroutines.test.*
class TickerTest {
@Test
fun refreshExposesTheError() = runTest {
val repository = FakeRepository(failure = IllegalStateException("offline"))
val ticker = Ticker(repository, backgroundScope)
ticker.refresh()
advanceUntilIdle()
assertEquals("offline", ticker.state.value.error)
}
}
// a handler on the scope catches what a child failed to handle
val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Default + CoroutineExceptionHandler { _, cause ->
System.err.println("unhandled: " + cause.message)
}
)⚠️
A
SupervisorJob stops one failed child from cancelling its siblings, but it does not swallow the exception. Install a CoroutineExceptionHandler, or the failure surfaces on the default handler and crashes the process.FAQ
When should I use a Channel instead of a SharedFlow?
Use a Channel when exactly one consumer should process each item, such as a work queue. Use a SharedFlow when several collectors should all receive every event, such as a navigation signal.
How do I test code that uses delays?
Use
runTest with a test dispatcher. Time is virtual, so advanceUntilIdle() and advanceTimeBy() skip the wait and the test finishes in milliseconds.Related
Functional Kotlin: lambdas, inline functions and DSLs Coroutines and collections
Last refreshed 2026-09-18.