Language and Runtime Concurrency Models: Kotlin
Kotlin runs on the JVM, so it inherits Java's threads and executors, but its signature feature is coroutines with structured concurrency by default: every coroutine lives in a scope, and cancelling or failing a scope stops its children.
Learning Objectives
Use JVM threads and executors from Kotlin; coroutines, suspending functions, dispatchers, structured concurrency, the Job lifecycle, cooperative cancellation, exception propagation and supervisor scopes.
Use channels and flows, coroutine Mutex and Semaphore; respect Android main-thread constraints; debug coroutines.
Plain-Language Explanation
Kotlin runs on the JVM, so it inherits Java's threads and executors, but its signature feature is coroutines with structured concurrency by default: every coroutine lives in a scope, and cancelling or failing a scope stops its children. Android uses them to keep the UI thread free.
The Kotlin examples are shown for comparison and were not run — no Kotlin compiler on the build machine. They target Kotlin 2.0 with kotlinx.coroutines 1.8.
Precise Technical Explanation
JVM threads and executors thread { }, Executors, java.util.concurrent — all available; Executor.asCoroutineDispatcher() bridges them.
Builders: launch (returns Job), async (returns Deferred<T>), runBlocking (bridge from blocking code; tests and main only).
suspend fun can call other suspending functions; compiled to continuation-passing state machines (stackless).
Dispatchers.Default (CPU, core-sized), Dispatchers.IO (blocking I/O, up to 64+ threads), Dispatchers.Main (UI thread on Android/desktop), Dispatchers.Unconfined; withContext(dispatcher) switches.
coroutineScope { } waits for all children and fails if any fails; CoroutineScope objects tie lifetimes to components (viewModelScope, lifecycleScope); GlobalScope is discouraged.
Job lifecycle New → Active → Completing → Completed, or Cancelling → Cancelled; join(), cancel(), isActive, invokeOnCompletion.
Cancellation and cooperative checks Suspending functions from kotlinx.coroutines check cancellation; CPU loops must call ensureActive()/yield()/isActive; CancellationException must not be swallowed; withContext(NonCancellable) for cleanup; withTimeout/withTimeoutOrNull.
Exception propagation and supervisor scopes A failing child cancels its parent and siblings; supervisorScope/SupervisorJob isolate children; CoroutineExceptionHandler handles uncaught exceptions in root launch coroutines; async defers exceptions to await().
Channels and flows Channel<T>(capacity) with send/receive (suspending), produce, select; Flow<T> — cold asynchronous streams with operators (map, buffer, conflate, flatMapMerge), backpressure by suspension; StateFlow/SharedFlow for hot state.
Mutex and semaphore APIs kotlinx.coroutines.sync.Mutex (withLock, suspends instead of blocking; not re-entrant) and Semaphore (withPermit).
Android main-thread constraints UI updates only on the main thread; network/disk I/O on the main thread throws NetworkOnMainThreadException/strict-mode violations; long main-thread work causes ANRs (~5 s). Pattern: launch on Dispatchers.Main (in viewModelScope), withContext(Dispatchers.IO) for I/O, update UI after.
-Dkotlinx.coroutines.debug adds coroutine names to thread names; DebugProbes.dumpCoroutines(); IntelliJ coroutine debugger; CoroutineName("…") contexts.
Key Terms
Function that can suspend without blocking Determines which threads run a coroutine Job / Deferred Handle to a coroutine / with a result
Waits for children; fails on child failure Children fail independently Cold asynchronous stream
Diagram or Execution Timeline
structuredSyntax and API Overview
suspend fun loadScreen(): Screen = coroutineScope {
val user = async(Dispatchers.IO) { api.user() } // concurrent children
val feed = async(Dispatchers.IO) { api.feed() }
Screen(user.await(), feed.await()) // failure of either cancels the other
}Complete Code Example
Structured concurrency, cancellation, supervision, channels and flows (not run; kotlinx.coroutines 1.8):
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun main() = runBlocking {
// fail-fast: a failing child cancels its sibling and the scope
try {
coroutineScope {
launch { try { delay(1_000) } finally { println("sibling cancelled, cleanup ran") } }
launch { delay(50); error("payments failed") }
}
} catch (e: IllegalStateException) { println("scope failed: ${e.message}") }
// supervision: failures stay local
supervisorScope {
val a = async { delay(20); "ok" }
val b = async { delay(10); error("boom") }
println("supervisor: a=${a.await()}, b failed=${runCatching { b.await() }.isFailure}")
}
// cooperative cancellation in a CPU loop
val job = launch(Dispatchers.Default) {
var i = 0L
while (true) { ensureActive(); i++ } // without ensureActive this loop could not be cancelled
}
delay(20); job.cancelAndJoin(); println("CPU job cancelled: ${job.isCancelled}")
// timeout
println("withTimeoutOrNull: ${withTimeoutOrNu
…Line-by-Line Explanation
coroutineScope cancels the sleeping sibling when the other fails (its finally runs) and rethrows.
supervisorScope lets a succeed though b failed; b's exception surfaces only at await().
ensureActive() is the cancellation point in a CPU loop.
withTimeoutOrNull returns null on timeout.
Mutex.withLock suspends rather than blocks; the counter reaches 1000.
The channel's capacity 2 makes the sender suspend until the receiver catches up.
The flow is cold: nothing runs until reduce collects it.
Output
Not run. By kotlinx.coroutines semantics it would print the cleanup and failure lines, a=ok, b failed=true, CPU job cancelled: true, withTimeoutOrNull: null, Mutex counter: 1000, channel: [1, 2, 3, 4, 5] and flow sum of even squares: 220.
Interleaving or State Trace
Fail-fast scope: t = 0 both children start; t = 50 ms child 2 throws → the scope's Job becomes Cancelling → child 1 receives CancellationException at delay → its finally runs → scope completes exceptionally → catch prints.
Correctness and Safety Analysis
Structured scopes guarantee no leaked coroutines; dispatchers decide threading, so shared state accessed from Dispatchers.Default coroutines still needs a Mutex, atomics or confinement.
Common Mistakes and Misconceptions
GlobalScope.launch (orphaned work).
Catching Exception around suspending calls and swallowing CancellationException.
Blocking calls on Dispatchers.Main or Default.
runBlocking in production code paths (blocks a thread).
Using java.util.concurrent locks inside coroutines across suspension points.
Failure Modes and Debugging Steps
Coroutine debug mode, DebugProbes, Android StrictMode for main-thread I/O, ANR traces, IntelliJ's coroutine tab.
Performance and Scalability Implications
Coroutines are cheap (hundreds of thousands are fine); Dispatchers.IO shares threads with Default but has a larger limit; flows with buffer/conflate control backpressure and latency.
Security Considerations
Tie coroutines to lifecycles so work stops when a user signs out or a screen closes; don't leak credentials through coroutine context elements.
Platform and Runtime Caveats
kotlinx.coroutines is a library (not the language); APIs marked @ExperimentalCoroutinesApi may change; Android's Dispatchers.Main needs kotlinx-coroutines-android.
Real-World Applications
Android apps (Jetpack viewModelScope, Room, Retrofit suspend APIs), Ktor servers, Spring with coroutines.
Best Practices
Structured scopes everywhere; no GlobalScope.
withContext(Dispatchers.IO) for blocking calls; keep Main free.
Make long loops cancellable; never swallow CancellationException.
Use supervisorScope where independent failures are expected.
Exercises
E1 · Beginner. Which dispatcher should a database call use on Android?
Answer: Dispatchers.IO (via withContext(Dispatchers.IO)), then return to Main to update the UI.
E2 · Intermediate. Why can't a while (true) { i++ } coroutine be cancelled, and what fixes it?
Answer: Cancellation is cooperative and the loop has no suspension or check; add ensureActive(), yield() or test isActive.
E3 · Advanced challenge. Implement "fetch from cache and network; show whichever arrives first, then update with the network result".
Answer: In a scope: val net = async { api.load() }; val cached = async { cache.load() }; select { cached.onAwait { show(it) }; net.onAwait { show(it) } }; then show(net.await()) if the cache won — and cancel cached if the network wins first.
Quiz
? What happens when a child launched inside coroutineScope throws?
+ Siblings are cancelled and the scope rethrows. :: Fail-fast by default.
- Only that child stops; the scope continues. :: That is supervisorScope.
- The exception is ignored. :: It propagates.
- The JVM exits. :: Not by default.
? Which builder returns a result to await?
+ async :: Returns Deferred<T>.
- launch :: Returns Job without a result.
- runBlocking in a coroutine :: It blocks a thread.
- GlobalScope :: Not a builder.
? Which are true? (select all that apply)
+ kotlinx.coroutines Mutex suspends instead of blocking. :: Suitable inside coroutines.
+ Flows are cold until collected. :: Nothing runs before collection.
- Dispatchers.Main is a thread pool for CPU work. :: It is the UI thread.
+ withContext(NonCancellable) lets cleanup suspend after cancellation. :: For essential cleanup.Summary
Kotlin adds coroutines with structured concurrency to the JVM: suspending functions, dispatchers, scopes and jobs, cooperative cancellation, supervision, channels and flows, coroutine-aware mutexes — with Android's main-thread rules shaping how they are used.
Further Reading
Kotlin docs: "Coroutines guide", "Flow", "Exceptions and cancellation".
Roman Elizarov, "Structured concurrency" (Medium, 2018).
Android Developers: "Kotlin coroutines on Android".
kotlinx.coroutines API reference.