What you'll learn
Flows are great for streams of data, but sometimes coroutines need to talk to each other more directly, or share a piece of mutable state safely. Channels are coroutine-safe queues, and Mutex and atomics protect shared state. Together they round out concurrent Kotlin.
By the end you'll be able to:
- Pass values between coroutines with a
Channel - Build producer-consumer pipelines with
produce - Protect shared mutable state with
Mutexand atomics - Understand the actor model at a high level
Channels
A Channel is conceptually a queue you can send to and receivefrom across coroutines — safely, without locks. send suspends if the channel is full and receive suspends if it's empty, so coroutines naturally throttle one another. close() signals that no more values are coming, ending a for loop over the channel:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (x in 1..3) channel.send(x * x) // send suspends if full
channel.close() // no more values coming
}
for (y in channel) println(y) // receive until closed
}Note
Rendezvous (default, no buffer), Buffered, Unlimited, and Conflated (keeps only the latest). The buffer choice decides when send suspends.Producers & consumers
The produce builder wires a coroutine to a channel of results, giving you a tidy producer that consumers pull from. This is the foundation of concurrent pipelines — one stage produces, the next consumes and transforms:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
// produce {} builds a coroutine that owns a channel of results.
fun CoroutineScope.squares(): ReceiveChannel<Int> = produce {
for (x in 1..5) send(x * x)
}
fun main() = runBlocking {
val squares = squares()
repeat(3) { println(squares.receive()) }
coroutineContext.cancelChildren() // stop the producer
}Tip
Sharing state safely
Concurrent code sharing mutable state invites race conditions: two coroutines updating the same variable can clobber each other. The simplest fix is to confine mutation to a single coroutine, but when you must share, a Mutex guarantees only one coroutine enters the critical section at a time — and unlike a thread lock, withLock suspends instead of blocking:
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
var counter = 0
val mutex = Mutex()
val jobs = List(1000) {
launch(Dispatchers.Default) {
mutex.withLock { counter++ } // one coroutine at a time
}
}
jobs.forEach { it.join() }
println("counter = $counter") // exactly 1000, no races
}Watch out
synchronized or Thread locks — they block the thread and can deadlock the dispatcher. Use the coroutine-aware Mutex, or atomic types like AtomicInteger for simple counters.Actors
The actor model combines both ideas: a single coroutine owns some private state and the outside world interacts with it only by sending messages through a channel. Because just one coroutine ever touches the state, there are no races and no locks — a very safe way to manage shared state. (Kotlin's actor builder is currently experimental, but the pattern is easy to reproduce by hand with a Channel and a loop.)
Key idea
Recap & quick check
Key takeaways
- A Channel is a coroutine-safe queue: send suspends when full, receive suspends when empty.
- close() a channel to end iteration; pick a capacity (rendezvous, buffered, conflated) to control back-pressure.
- produce {} builds a producer coroutine that consumers receive from — the base of pipelines.
- Guard shared mutable state with a suspending Mutex (withLock) or atomics — never Thread locks.
- The actor pattern confines state to one coroutine reached only by messages — race-free by design.
Quick check
1. What happens when you call send() on a full rendezvous channel?
2. How do you signal that no more values will be sent on a channel?
3. Why use Mutex instead of synchronized in coroutines?
4. The actor pattern avoids races by...
5. When is a Flow a better fit than a Channel?
That completes Phase 6 — you can write concurrent, non-blocking Kotlin with coroutines, flows, and channels. Next, Phase 7 — Professional & Applied Kotlinbegins with living alongside the JVM's biggest language: Java interoperability.