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

structured

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