Chapter 14: Coroutines
The introduction explains why applications need asynchronous work and identifies coroutines as Kotlin’s approach to it.
After this chapter you will be able to
- Need for asynchronous operations. Explain why applications need operations to run independently without blocking one another, while still coordinating concurrent tasks.
- Concurrency and parallelism distinction. Distinguish interleaved concurrent execution from physically simultaneous parallel execution. Explain how switching between tasks on one core can keep a user interface responsive during a heavy computation, and state that Kotlin coroutines can support both kinds of computation.
- Coroutine concurrency benefits. Explain that Kotlin coroutines support asynchronous, non-blocking concurrent code, are lighter weight than traditional threads, and offer facilities for managing concurrent tasks and their life cycle through structured concurrency.
- JVM thread costs and limits. Explain why JVM threads are useful for concurrent and parallel work but costly to create and use for fine-grained or waiting tasks.
- Lightweight coroutine execution. Describe how coroutines offer fine-grained concurrent work and can suspend without blocking system resources, while still running on JVM threads.
- Structured coroutine task hierarchy. Explain the contrast between standalone threads and the hierarchy established by structured concurrency for coroutine tasks.
- Sequential Shape of Coroutine Code. Explain that code using Kotlin coroutines can often retain a sequential-looking shape, unlike code written with some other concurrency approaches.
- Sequential code with suspension. Describe how marking the example functions with suspend changes waiting behavior without changing the order in which showUserInfo is written and executed.
- Coroutine simplicity for successive calls. Explain the section’s comparison between coroutine-based code and the other approaches for this basic concurrency case.
- Suspending caller requirement. Explain why a suspending function must be called from code that can also suspend, while a suspending function can still call regular functions.
- First suspending call entry points. Describe the two entry points identified for a first suspending call: marking a program’s main function with suspend or using a coroutine builder, and explain why the main-function approach is typically limited to small utilities.
- Coroutine as suspendable computation. Explain that a coroutine is an instance of a suspendable computation that can run concurrently with other coroutines and provides the machinery needed to suspend functions called in its body.
- Coroutine builder purposes. Distinguish the stated purposes of runBlocking, launch, and async, and state that coroutine builder functions create coroutines.
- runBlocking as a bridge. Explain how runBlocking starts a coroutine from regular code and allows its block to call suspending functions.
- Launch and its Job handle. Explain that launch starts a child coroutine for work whose computed return value is not needed, and that it returns a Job handle rather than the coroutine’s computed result.
- Single-thread coroutine interleaving. Trace how the parent coroutine and two launched children run in the example: one child suspends at delay, the other runs, and the first later resumes. Explain why this is concurrency without parallel execution.
- Async results and awaiting. Trace how async starts a coroutine, returns a Deferred<T>, and lets the caller obtain its result with the suspending await function. Explain why two independent computations can finish in roughly the time of the longer one.
- Choosing async for independent tasks. Distinguish the concurrent, result-waiting case that calls for async from basic sequential calls to suspending functions.
- Dispatcher control of execution. Explain how choosing a dispatcher affects the thread or threads a coroutine uses, including what can happen when it resumes after suspension.
- Dispatcher selection and inheritance. Explain that a coroutine normally inherits its parent’s dispatcher, so a dispatcher need not be specified every time; choose one explicitly when the work has particular execution requirements.
- Default dispatcher for ordinary work. Describe Dispatchers.Default as the usual general-purpose choice and explain how its CPU-core-sized thread pool can run coroutines across threads.
- Main dispatcher for UI operations. Explain why operations constrained to an application’s UI thread can use Dispatchers.Main, and why its implementation depends on the UI framework.
- IO dispatcher for blocking APIs. Explain why blocking API calls can exhaust Dispatchers.Default’s limited pool and how Dispatchers.IO addresses that risk.
- Dispatcher argument to coroutine builders. Explain that passing a dispatcher to a coroutine builder selects where its coroutine runs, using the launch example to distinguish the main thread from a default-dispatcher thread.
- Dispatcher switching with withContext. Explain how withContext switches the dispatcher of an existing coroutine, using the section’s example of background work followed by a UI update.
- Single versus shared coroutine data. Explain why one coroutine incrementing a counter 10,000 times produces the expected result, while 10,000 coroutines incrementing a shared counter may not.
- Shared state protection options. Describe the approaches this section gives for protecting data modified by multiple coroutines running in parallel.
- Coroutine context elements. Explain that each coroutine carries a CoroutineContext made up of elements, including a dispatcher and usually a Job, with optional metadata such as a name or exception handler.
- Context element overrides. Explain that passing a context element to a coroutine builder or withContext overrides that element in the child coroutine’s context, and that the + operator combines elements to specify multiple overrides.
Section by section
Introduction
The introduction explains why applications need asynchronous work and identifies coroutines as Kotlin’s approach to it.
- Need for asynchronous operations: Explain why applications need operations to run independently without blocking one another, while still coordinating concurrent tasks.
Why it matters: This is the problem Kotlin’s coroutine approach is meant to address. - Coroutines for concurrent work: State that Kotlin uses coroutines for asynchronous computations and that code using them can be concurrent and parallel.
Why it matters: It connects the need for asynchronous work to the Kotlin feature explored in the chapter.
Key terms: asynchronously, concurrent tasks, blocking, coordinate and synchronize, coroutines, concurrent, parallel
14.1 Concurrency vs. parallelism
This section distinguishes concurrency from parallelism and explains how tasks can be concurrent even on a single CPU core.
- Concurrency and parallelism distinction: Distinguish interleaved concurrent execution from physically simultaneous parallel execution. Explain how switching between tasks on one core can keep a user interface responsive during a heavy computation, and state that Kotlin coroutines can support both kinds of computation.
Why it matters: The distinction helps you tell whether tasks are taking turns or actually running at the same time.
Key terms: concurrency, parallelism, CPU core, Kotlin coroutines
14.2 Concurrency the Kotlin way: Suspending functions and coroutines
Introduces coroutines as a lightweight, non-blocking way to write concurrent Kotlin code and suspending functions as a way to make that code look sequential.
- Coroutine concurrency benefits: Explain that Kotlin coroutines support asynchronous, non-blocking concurrent code, are lighter weight than traditional threads, and offer facilities for managing concurrent tasks and their life cycle through structured concurrency.
Why it matters: These are the reasons the chapter presents coroutines as an approach to concurrent code. - Sequential-looking suspending functions: Describe suspending functions as the basic coroutine abstraction introduced here: they let concurrent code look sequential without blocking threads.
Why it matters: It connects the chapter’s non-blocking concurrency claim to the way coroutine code can be written.
Key terms: coroutines, non-blocking, structured concurrency, suspending functions, blocking threads
14.3 Comparing threads and coroutines
This section compares the costs and limits of JVM threads with Kotlin coroutines, then explains how coroutines differ from and may complement Project Loom virtual threads.
- JVM thread costs and limits: Explain why JVM threads are useful for concurrent and parallel work but costly to create and use for fine-grained or waiting tasks.
Why it matters: These costs explain why starting a thread for every small or waiting task is often impractical. - Lightweight coroutine execution: Describe how coroutines offer fine-grained concurrent work and can suspend without blocking system resources, while still running on JVM threads.
Why it matters: This is why coroutines suit large numbers of small or asynchronous tasks without replacing threads underneath. - Structured coroutine task hierarchy: Explain the contrast between standalone threads and the hierarchy established by structured concurrency for coroutine tasks.
Why it matters: A task hierarchy makes concurrent work easier to manage when part of it fails or is no longer needed. - Coroutines and Loom distinctions: Distinguish Kotlin coroutines from Project Loom virtual threads in their aims and API design, and state how they may work together.
Why it matters: Virtual threads address some of the same costs without making coroutines' API design or explicit suspending operations redundant.
Key terms: threads, system threads, blocked, coroutines, suspendable computations, JVM threads, structured concurrency, hierarchy, children, cancellation
14.4 Functions that can pause: Suspending functions
Introduces the sequential-looking style of Kotlin coroutine code and identifies suspending functions as functions that can pause.
- Sequential Shape of Coroutine Code: Explain that code using Kotlin coroutines can often retain a sequential-looking shape, unlike code written with some other concurrency approaches.
Why it matters: This is the motivation for learning about suspending functions without assuming how they work yet.
Key terms: Kotlin coroutines, suspending functions, sequential
14.4.1 Code written with suspending functions looks sequential
Shows how suspending functions preserve sequential-looking application code while avoiding the costs of blocking a thread during network waits.
- Blocking network request costs: Explain why a sequential implementation that waits for network requests can harm a service or user interface.
Why it matters: The cost of waiting explains why this example needs a non-blocking approach. - Sequential code with suspension: Describe how marking the example functions with suspend changes waiting behavior without changing the order in which showUserInfo is written and executed.
Why it matters: You can express the network-dependent steps in order without keeping a thread occupied throughout each wait. - Coroutine-aware library requirement: State what must also be true of the network-call implementations for the example to be non-blocking.
Why it matters: A suspend modifier alone does not make a blocking library call non-blocking.
Key terms: blocking, thread, network request, suspend modifier, suspending functions, suspension, underlying libraries, Kotlin coroutines, login, loadUserData
14.5 Comparing coroutines to other approaches
The section compares callbacks, futures, and reactive streams with Kotlin coroutines for expressing successive concurrent operations.
- Nested callback complexity: Explain why implementing successive calls with callbacks changes function signatures and can make growing code hard to read.
Why it matters: It gives you a concrete reason to prefer a sequential-looking way of writing successive calls. - Future and stream overhead: Describe how futures and reactive streams avoid nested callbacks while introducing wrapped return values and operators.
Why it matters: Avoiding callback nesting does not necessarily avoid extra concepts in function declarations and calls. - Coroutine simplicity for successive calls: Explain the section’s comparison between coroutine-based code and the other approaches for this basic concurrency case.
Why it matters: This is the section’s main reason for using coroutines in the basic example, without claiming futures or reactive streams have no use.
Key terms: callbacks, callback hell, CompletableFuture, reactive streams, RxJava, Single, suspend modifier, coroutines, sequential look
14.5.1 Calling a suspending function
Explains where suspending functions can be called and how a program can make its first such call.
- Suspending caller requirement: Explain why a suspending function must be called from code that can also suspend, while a suspending function can still call regular functions.
Why it matters: This explains why a suspending call cannot simply be inserted into any existing function. - First suspending call entry points: Describe the two entry points identified for a first suspending call: marking a program’s main function with suspend or using a coroutine builder, and explain why the main-function approach is typically limited to small utilities.
Why it matters: It connects the calling restriction to practical ways of starting suspending code.
Key terms: suspending function, suspend modifier, non-suspending code, main function, coroutine builder functions
14.6 Entering the world of coroutines: Coroutine builders
This section defines a coroutine, introduces the purposes of three coroutine builders, and explains where most coroutine APIs come from.
- Coroutine as suspendable computation: Explain that a coroutine is an instance of a suspendable computation that can run concurrently with other coroutines and provides the machinery needed to suspend functions called in its body.
Why it matters: This explains why a suspending function can be called from a coroutine. - Coroutine builder purposes: Distinguish the stated purposes of runBlocking, launch, and async, and state that coroutine builder functions create coroutines.
Why it matters: The three builders serve different purposes, even though each is a way to create a coroutine. - Core and library division: Explain why most coroutine functions discussed here come from kotlinx.coroutines rather than Kotlin’s standard library.
Why it matters: It clarifies why a project needs a separate library for most of the coroutine functionality discussed in the book.
Key terms: coroutine, suspendable computation, suspending function, coroutine builder functions, runBlocking, launch, async, Kotlin standard library, kotlinx.coroutines
14.6.1 From regular code into the realm of coroutines: The runBlocking function
This section shows how runBlocking lets regular code call suspending functions and explains why its blocking behavior can still be useful.
- runBlocking as a bridge: Explain how runBlocking starts a coroutine from regular code and allows its block to call suspending functions.
Why it matters: This explains how code outside a coroutine can reach a suspending function. - Blocking and child coroutines: Explain why blocking one thread with runBlocking does not mean each additional child coroutine needs another blocked thread.
Why it matters: It clarifies the apparent conflict between runBlocking and coroutines’ ability to avoid blocking more threads.
Key terms: runBlocking, coroutine builder, suspending functions, child coroutines, launch, suspended
14.6.2 Creating start-and-forget coroutines: The launch function
This section shows how launch starts child coroutines for work without a return value and how suspension lets them take turns on one thread.
- Launch and its Job handle: Explain that launch starts a child coroutine for work whose computed return value is not needed, and that it returns a Job handle rather than the coroutine’s computed result.
Why it matters: This helps you choose launch when you want work to run without collecting a result. - Single-thread coroutine interleaving: Trace how the parent coroutine and two launched children run in the example: one child suspends at delay, the other runs, and the first later resumes. Explain why this is concurrency without parallel execution.
Why it matters: This explains how several coroutines can make progress even when the example uses only one thread. - Stored state during suspension: Describe, at the level explained here, how a suspended coroutine can later continue: compiler-generated support stores its state in memory and uses it to restore execution.
Why it matters: Stored state explains how a coroutine can pause without occupying the thread and still continue later.
Key terms: launch, child coroutine, start-and-forget, Job, runBlocking, delay, suspension point, interleaved execution, suspending functions, state
14.6.3 Awaitable computations: The async builder
Explains how async starts concurrent computations whose results can be awaited, when to use it instead of ordinary suspending calls, and where async/await functionality lives in Kotlin.
- Async results and awaiting: Trace how async starts a coroutine, returns a Deferred<T>, and lets the caller obtain its result with the suspending await function. Explain why two independent computations can finish in roughly the time of the longer one.
Why it matters: This lets you start independent work together and wait for its results without treating async itself as a blocking call. - Choosing async for independent tasks: Distinguish the concurrent, result-waiting case that calls for async from basic sequential calls to suspending functions.
Why it matters: It helps you avoid adding async when straightforward suspending calls already express the work. - Library status of async await: State that async and await are library functions rather than Kotlin keywords, and relate this to suspending functions as a core language primitive.
Why it matters: It clarifies the distinction between Kotlin’s built-in suspension support and the coroutine operations supplied by its library.
Key terms: async, Deferred<T>, await, concurrently, suspending function calls, independent tasks, kotlinx.coroutines, suspending functions
14.7 Deciding where your code should run: Dispatchers
This section explains how dispatchers determine the threads a coroutine uses and how thread pools reuse threads to run tasks.
- Dispatcher control of execution: Explain how choosing a dispatcher affects the thread or threads a coroutine uses, including what can happen when it resumes after suspension.
Why it matters: This distinction helps you reason about where coroutine code runs without assuming a coroutine owns a thread. - Thread pool reuse: Describe how a thread pool runs incoming tasks using threads it already manages rather than allocating a new thread for every task.
Why it matters: It clarifies what it means to dispatch coroutines to a thread pool.
Key terms: dispatcher, coroutine, thread, thread pool, threads, tasks
14.7.1 Choosing a dispatcher
This section explains how to choose among Kotlin’s built-in coroutine dispatchers based on ordinary work, UI-thread requirements, and blocking APIs.
- Dispatcher selection and inheritance: Explain that a coroutine normally inherits its parent’s dispatcher, so a dispatcher need not be specified every time; choose one explicitly when the work has particular execution requirements.
Why it matters: You can make an explicit dispatcher choice when it serves a purpose, rather than treating it as required for every coroutine. - Default dispatcher for ordinary work: Describe Dispatchers.Default as the usual general-purpose choice and explain how its CPU-core-sized thread pool can run coroutines across threads.
Why it matters: This explains both why Default suits ordinary coroutine work and why coroutine count need not match thread count. - Main dispatcher for UI operations: Explain why operations constrained to an application’s UI thread can use Dispatchers.Main, and why its implementation depends on the UI framework.
Why it matters: UI updates may require a particular thread rather than whichever thread runs ordinary coroutine work. - IO dispatcher for blocking APIs: Explain why blocking API calls can exhaust Dispatchers.Default’s limited pool and how Dispatchers.IO addresses that risk.
Why it matters: Choosing IO keeps blocking library calls from tying up the limited pool used for ordinary coroutine work.
Key terms: dispatcher, parent coroutine, Dispatchers.Default, thread pool, suspends, Dispatchers.Main, UI thread, main thread, Dispatchers.IO, blocking API
14.7.2 Passing a dispatcher to a coroutine builder
This section shows how to run an entire coroutine on a chosen dispatcher by passing it to a coroutine builder.
- Dispatcher argument to coroutine builders: Explain that passing a dispatcher to a coroutine builder selects where its coroutine runs, using the launch example to distinguish the main thread from a default-dispatcher thread.
Why it matters: It lets you choose where a coroutine runs when you start it.
Key terms: dispatcher, coroutine builder, launch, default dispatcher
14.7.3 Using withContext to switch the dispatcher within a coroutine
This section shows how an existing coroutine can switch dispatchers to perform a UI update after background work.
- Dispatcher switching with withContext: Explain how withContext switches the dispatcher of an existing coroutine, using the section’s example of background work followed by a UI update.
Why it matters: It lets you keep long-running work off the UI thread while doing the eventual UI update there. - Main dispatcher availability: State when the example using Dispatchers.Main can run, and contrast it with the section’s Dispatchers.IO example.
Why it matters: The UI-thread example will not work unchanged in a project without a configured UI framework.
Key terms: withContext, dispatcher, Dispatchers.Main, Dispatchers.IO, UI framework
14.7.4 Coroutines and dispatchers aren’t a magical fix for thread-safety concerns
This section explains why a single coroutine avoids internal parallelism but shared data modified by parallel coroutines still needs thread-safety measures.
- Single versus shared coroutine data: Explain why one coroutine incrementing a counter 10,000 times produces the expected result, while 10,000 coroutines incrementing a shared counter may not.
Why it matters: Using coroutines does not by itself make shared mutable data safe. - Shared state protection options: Describe the approaches this section gives for protecting data modified by multiple coroutines running in parallel.
Why it matters: These options let you avoid lost updates when coroutines share mutable data.
Key terms: multithreaded dispatcher, coroutine, thread-safety, synchronization, Mutex, critical section, AtomicInteger, ConcurrentHashMap, single-threaded dispatcher, withContext
14.8 Coroutines carry additional information in their coroutine context
Explains what a coroutine context contains, how to inspect it, and how to supply context elements when starting or switching coroutines.
- Coroutine context elements: Explain that each coroutine carries a CoroutineContext made up of elements, including a dispatcher and usually a Job, with optional metadata such as a name or exception handler.
Why it matters: It explains why functions that accept a dispatcher actually take a CoroutineContext. - Current context inspection: Describe how a suspending function can access its current coroutine context through the coroutineContext property.
Why it matters: It gives you a way to inspect the context carried by the current coroutine. - Context element overrides: Explain that passing a context element to a coroutine builder or withContext overrides that element in the child coroutine’s context, and that the + operator combines elements to specify multiple overrides.
Why it matters: It lets you read calls that specify both where a coroutine runs and other context information.
Key terms: CoroutineContext, CoroutineDispatcher, Job, CoroutineName, CoroutineExceptionHandler, suspending function, compiler intrinsic, + operator, coroutine builder, withContext
14.9 Summary
Where this is used later
- Coroutine scopes establish structure between coroutines (chapter 15) · Develops the task structure only identified here.
- (chapter intro) (chapter 16) · Introduces coroutines as a Kotlin concurrency abstraction.
- Cancellation cascades through all children (chapter 15) · Develops how cancellation cascades through children.
- Creating a coroutine scope: The coroutineScope function (chapter 15) · Prior use of coroutine builders to create coroutines.
- Handling errors thrown inside coroutines (chapter 18) · Using runBlocking as a coroutine builder.
- Testing coroutines and flows (chapter 18) · Introduces runBlocking as a bridge from regular code into coroutines.
- Triggering cancellation (chapter 15) · Uses the idea that a Job is a handle for a coroutine when discussing cancellation.
- Associating coroutine scopes with components: CoroutineScope (chapter 15) · Launching a coroutine.
Concepts explained in this chapter
- suspending functionA suspending function is a function that can pause its execution without blocking its underlying thread and later resume from the same logical point.
- coroutineA coroutine is a lightweight suspendable computation that can pause its execution and resume it later without blocking its underlying thread.
- dispatcherA dispatcher is a coroutine execution-context component that determines which thread or threads execute a coroutine and where its continuations are resumed.
- threadA thread is an independently schedulable sequence of execution within a process.
- suspendable computationA suspendable computation is a computation whose execution can be paused and later resumed while preserving its logical progress.
- structured concurrencyStructured concurrency is a concurrency model that scopes concurrent tasks so their lifetimes and failures are managed through a shared parent-child hierarchy.
- launchlaunch is a kotlinx.coroutines coroutine builder that starts a coroutine whose computation does not produce a result value.
- coroutine builder functionA coroutine builder function is an API function that creates and starts a coroutine with specified execution, context, and result behavior.
- Dispatchers.MainDispatchers.Main is a coroutine dispatcher that schedules coroutine work on the application's main or UI thread.
- futureA future is an abstraction representing a result that may become available after an asynchronous computation finishes.
- JobJob is a coroutine-context element that represents the lifecycle of a coroutine and propagates child failure to its parent under regular structured concurrency.
- Dispatchers.DefaultDispatchers.Default is a Kotlin coroutines dispatcher backed by a shared pool of worker threads for general-purpose coroutine execution.
- Dispatchers.IODispatchers.IO is the Kotlin coroutines dispatcher backed by an automatically scaling thread pool for blocking input/output operations.
- delay()delay is a coroutine-suspending function that pauses a coroutine for a specified duration without blocking its underlying thread.
- kotlinx.coroutineskotlinx.coroutines is JetBrains' Kotlin library providing coroutine builders, synchronization primitives, and other concurrency APIs.
- suspension pointA suspension point is a point at which a suspending coroutine can pause and allow other coroutines sharing its dispatcher to execute.
- concurrencyConcurrency is the execution of multiple pieces of computation during overlapping periods of time, whether or not they execute physically at the same instant.
- coroutine contextcoroutineContext is a compiler-provided property that exposes the context of the currently executing coroutine from a suspending function.
- asyncasync is a coroutine builder that starts an asynchronous computation and produces a handle for obtaining its result.
- async/awaitawait is an operation that suspends a coroutine until an asynchronous computation's result is available.
- runBlockingrunBlocking is a kotlinx.coroutines coroutine builder that blocks the current thread while executing a coroutine and its suspending operations.
- resource leakA resource leak is the unintended retention or failure to release of resources such as threads, memory, or active concurrent tasks.
- child coroutineA child coroutine is a coroutine launched within the scope of another coroutine and associated with that parent coroutine.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
