Coroutines | Kotlin - Wyatt's Notes
Core Concepts
Section titled “Core Concepts”Coroutines are Kotlin’s mechanism for asynchronous programming. They are lightweight — a coroutine Suspends instead of blocking a thread, allowing a small number of OS threads to handle many Concurrent operations.
Key distinction: coroutines are not threads. A coroutine runs on a thread but can be suspended And resumed on a different thread. Thousands of coroutines can run concurrently on a handful of Threads.
Add the coroutines dependency:
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.9.0") // Reactor testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")}Suspend Functions
Section titled “Suspend Functions”A suspend function can suspend the execution of a coroutine without blocking the underlying Thread. Only coroutines (or other suspend functions) can call suspend functions.
suspend fun fetchUser(id: Long): User { delay(1000) // suspends without blocking the thread return api.getUser(id)}suspend functions have no special mechanism — the compiler inserts a state machine at compile Time that saves and restores the local variables and execution position at each suspension point.
delay() is the coroutine equivalent of Thread.sleep() but does not block the thread.
Coroutine Builders
Section titled “Coroutine Builders”launch
Section titled “launch”Launches a fire-and-forget coroutine. Returns a Job that can be joined or cancelled.
fun main() = runBlocking { val job: Job = launch { delay(1000) println("World") } println("Hello") job.join() println("Done")}// Output: Hello (immediate), World (after 1s), DoneLaunches a coroutine that returns a Deferred<T> (a cancellable future). Use await() to get the Result.
suspend fun fetchAllData(): Data { val deferred1 = async { fetchUser(1) } val deferred2 = async { fetchOrders(1) } val deferred3 = async { fetchPreferences(1) }
return Data( user = deferred1.await(), orders = deferred2.await(), preferences = deferred3.await() )}launch vs async:
launch: returnsJobNo result value. Use for side effects.async: returnsDeferred<T>Produces a result. Use for concurrent computations.
runBlocking
Section titled “runBlocking”Bridges blocking and non-blocking code. Blocks the current thread until all coroutines inside Complete. Primarily used in main() and tests — avoid in application code.
fun main() = runBlocking { launch { delay(1000) println("Done") }}withContext
Section titled “withContext”Switches the coroutine to a different dispatcher and returns a result.
suspend fun loadData(): String = withContext(Dispatchers.IO) { // runs on IO thread pool Thread.sleep(1000) // blocking IO -- OK on Dispatchers.IO "data"}Dispatchers
Section titled “Dispatchers”Dispatchers determine which thread pool executes the coroutine.
| Dispatcher | Purpose |
|---|---|
Dispatchers.Default | CPU-intensive work (sorting, parsing, JSON) |
Dispatchers.IO | Blocking IO (network, disk, database) |
Dispatchers.Main | UI thread (Android, JavaFX) |
Dispatchers.Unconfined | Starts on caller thread, resumes on whatever thread resumes it |
launch(Dispatchers.Default) { // CPU work}
launch(Dispatchers.IO) { // blocking IO}
withContext(Dispatchers.Main) { // update UI}Structured Concurrency
Section titled “Structured Concurrency”Coroutines follow structured concurrency: child coroutines are bound to a parent scope. When the Parent scope is cancelled, all children are cancelled. When all children complete, the parent Completes.
suspend fun processBatch(items: List<Item>) = coroutineScope { items.map { item -> async { processItem(item) } }.awaitAll()}If any processItem call throws, coroutineScope cancels all other coroutines and propagates the Exception. This prevents resource leaks from orphaned coroutines.
CoroutineScope
Section titled “CoroutineScope”coroutineScope creates a new scope that completes when all children complete. It does not block The current thread — it suspends.
supervisorScope is a variant where the failure of one child does not cancel the others.
suspend fun fetchWithFallback() = supervisorScope { val primary = async { fetchFromPrimary() } val fallback = async { fetchFromFallback() }
try { primary.await() } catch (e: Exception) { fallback.await() }}Cancellation
Section titled “Cancellation”Coroutines cooperate with cancellation by checking isActive or calling cancellable suspend Functions.
launch { repeat(1000) { i -> if (!isActive) return@launch // cooperative cancellation check
// delay() is cancellable -- throws CancellationException on cancel delay(100) println("Processing $i") }}CancellationException Propagation
Section titled “CancellationException Propagation”CancellationException is special — it is used to cancel coroutines and should not be caught in General catch blocks.
launch { try { delay(5000) } catch (e: CancellationException) { throw e // always rethrow CancellationException } catch (e: Exception) { // handle other exceptions }}Flow is a cold asynchronous stream. It is the coroutine equivalent of RxJava’s Observable but Built on coroutines.
fun numbers(): Flow<Int> = flow { for (i in 1..5) { delay(100) emit(i) }}
suspend fun main() { numbers() .map { it * it } .filter { it > 10 } .collect { println(it) }}// Output: 16, 25Flow is cold — nothing happens until collect is called. Each collector gets its own independent Stream.
Flow Operators
Section titled “Flow Operators”flow { emit(1) emit(2) emit(3)} .map { it * 2 } // [2, 4, 6] .filter { it > 2 } // [4, 6] .onEach { println(it) } // side effects .catch { e -> log(e) } // upstream exception handling .onCompletion { println("done") } .collect()StateFlow and SharedFlow
Section titled “StateFlow and SharedFlow”StateFlow is a hot flow that holds a single current value. Use it for state management.
class ViewModel { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateName(name: String) { _uiState.update { it.copy(userName = name) } }}SharedFlow is a hot flow for event broadcasting.
class EventBus { private val _events = MutableSharedFlow<Event>(extraBufferCapacity = 64) val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) { _events.emit(event) }}Channels
Section titled “Channels”Channels provide a way to transfer values between coroutines. They are similar to Go channels.
suspend fun producer(channel: SendChannel<Int>) { for (i in 1..5) { channel.send(i) } channel.close()}
suspend fun consumer(channel: ReceiveChannel<Int>) { for (value in channel) { println(value) }}Channel Types
Section titled “Channel Types”| Type | Description |
|---|---|
Channel() | Rendezvous (buffer size 0) |
Channel(64) | Buffered with capacity 64 |
Channel.CONFLATED | Drops previous values, keeps latest |
Channel.UNLIMITED | Unbounded buffer (use with caution) |
Common Pitfalls
Section titled “Common Pitfalls”- ** Using
runBlockingin application code. It blocks the thread, defeating the purpose of coroutines. UseCoroutineScopewith lifecycle management instead. - ** Forgetting to make IO functions suspending. If you wrap blocking calls in
withContext(IO)that is fine, but prefer genuinely non-blocking libraries (e.g.,ktor-clientRetrofit with coroutines) overwithContext(IO)wrapping blocking code. - ** Not handling cancellation properly. Always rethrow
CancellationException. UseensureActive()or checkisActivein long-running CPU loops. - ** Using
GlobalScope. It creates unbound coroutines that outlive the application scope, making cancellation and lifecycle management impossible. Always use structured concurrency with explicit scopes. - ** Collecting multiple flows sequentially. Use
combine``mergeOrzipto compose flows concurrently.
flowchart TD
A[Coroutines] --> B[Key Concepts]
A --> C[Core Principles]
A --> D[Practical Applications]
B --> E[Fundamental definitions]
C --> F[Design patterns]
D --> G[Real-world usage]Summary
Section titled “Summary”This topic covers the core concepts of coroutines, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Intuition
Section titled “Intuition”Coroutines are Kotlin’s answer to asynchronous programming without the complexity of callbacks or reactive streams. A suspend function can pause execution without blocking its thread, and the compiler generates a state machine that saves and restores local variables at each suspension point. Structured concurrency ensures that child coroutines are bound to their parent scope — when the parent is cancelled or fails, all children are cancelled too, preventing resource leaks. Dispatchers control which thread pool runs each coroutine: Default for CPU work, IO for blocking operations, Main for UI. Flow is a cold asynchronous stream that processes elements lazily, similar to RxJava’s Observable but built entirely on coroutines.
Cross-References
Section titled “Cross-References”- Functions — suspend functions and higher-order functions
- Classes and Objects — sealed classes for flow state
- Testing — testing coroutines with runTest