Chapter 15: Structured concurrency
This introduction explains why applications need to track coroutine lifetimes and presents structured concurrency as Kotlin’s built-in way to do so.
After this chapter you will be able to
- Coroutine hierarchies and lifetimes. Explain that structured concurrency organizes coroutines into a hierarchy so an application can manage and keep track of their lifetimes without manually tracking every coroutine.
- Coroutine scope parent-child relationships. Explain that each coroutine belongs to a coroutine scope and that calling launch or async inside another coroutine builder automatically makes the new coroutine a child of that coroutine.
- Parent waits for child coroutines. Describe why runBlocking can finish executing its own body yet keep the program running until its child coroutines finish, without manually tracking or awaiting them.
- Coroutine scope completion. Explain that coroutineScope is a suspending function that creates a scope without creating a new coroutine, and completes only after all its child coroutines finish.
- Two coroutine scope functions. Distinguish the suspending coroutineScope function from the CoroutineScope constructor function by what each is for and whether it waits.
- Component scope life cycle. Describe how a component uses its own CoroutineScope to start coroutines and end them when the component stops.
- GlobalScope and coroutine hierarchy. Explain why coroutines launched in GlobalScope are not children of the coroutine that launches them, and describe the resulting behavior in the runBlocking example.
- GlobalScope lifecycle and scope choice. Explain the risks of using GlobalScope in application code and describe the section’s advice on choosing a scope instead.
- Child coroutine context construction. Trace how a new coroutine receives its context: it inherits the parent context, creates a new Job linked to the parent's Job, and then applies any supplied context arguments, which can override inherited elements.
- Inherited dispatcher selection. Explain which dispatcher a new coroutine uses when none is specified and distinguish that from assuming it always uses Dispatchers.Default.
- Cancellation and unnecessary work. Explain that cancellation stops code before normal completion and prevents work whose result is no longer needed, such as a request after a user navigates away.
- Coroutine cancellation handles. Explain how to trigger cancellation of a coroutine created with launch or async, and how to cancel a coroutine scope.
- Time-limited coroutine computations. Explain that withTimeout and withTimeoutOrNull constrain how long a computation may run: an expired timeout cancels the computation, while one that finishes in time produces its value.
- Timeout result and exception handling. Distinguish how withTimeout and withTimeoutOrNull report an expired timeout, and explain why an uncaught exception from withTimeout can be undesirable.
- Cascading child coroutine cancellation. Explain that cancelling a coroutine cancels all its child coroutines, including descendants several levels down, so they do not keep running after their parent is cancelled.
- Cancellation at suspension points. Explain that cancellation is delivered by throwing a CancellationException, primarily at suspension points, and relate this to the possible output of the A/B/C example.
- Preserving cancellation exceptions. Explain why a broad catch can prevent cancellation and how to change the catch so cancellation proceeds.
- Cooperative cancellation in suspending code. Explain why marking CPU-heavy work as suspend does not make it cancellable, and how calling a cancellable function changes that.
- Coroutine cancellation status checks. Distinguish checking a CoroutineScope’s isActive property from calling ensureActive: a false isActive value lets the coroutine finish work, release resources, and return, while ensureActive throws a CancellationException when the coroutine is no longer active.
- Yield and coroutine interleaving. Explain why CPU-heavy coroutines without suspension points may run one after another, and how yield changes that behavior while also allowing cancellation.
- Resource cleanup during cancellation. Explain how cancellation can leave a coroutine’s resource open, and describe how a finally block or, for an AutoClosable resource, the use function ensures cleanup.
- Framework-managed scope lifetimes. Explain how frameworks can provide coroutine scopes and cancel them when the associated component ends. Describe how Android's viewModelScope is cancelled when its ViewModel is cleared, along with coroutines launched in it.
- Ktor request and application scopes. Distinguish Ktor's request-level scope from its application scope, and explain which to choose for work that should stop on client disconnection versus work that should continue independently of the request.
What it builds on
- Creating start-and-forget coroutines: The launch function (chapter 14) · Introduces creating coroutines with launch.
- Awaitable computations: The async builder (chapter 14) · Introduces creating coroutines with async.
- From regular code into the realm of coroutines: The runBlocking function (chapter 14) · Introduces runBlocking as a coroutine builder.
- Entering the world of coroutines: Coroutine builders (chapter 14) · Prior use of coroutine builders to create coroutines.
- Functions that can pause: Suspending functions (chapter 14) · The meaning of a suspending function.
- Passing a dispatcher to a coroutine builder (chapter 14) · Passing a dispatcher when starting coroutines.
- Coroutines carry additional information in their coroutine context (chapter 14) · The concept of a coroutine context and its elements.
- Handling exceptions and recovering from errors: try, catch, and finally (chapter 2) · Introduces try and catch exception handling.
Section by section
Introduction
This introduction explains why applications need to track coroutine lifetimes and presents structured concurrency as Kotlin’s built-in way to do so.
- Coroutine hierarchies and lifetimes: Explain that structured concurrency organizes coroutines into a hierarchy so an application can manage and keep track of their lifetimes without manually tracking every coroutine.
Why it matters: It gives you a way to reason about many coroutines without treating each one as an unrelated task. - Cost of untracked coroutines: Describe why losing track of coroutines can waste work or resources, using the example of a user leaving a screen while a network request is still being processed.
Why it matters: Tracking coroutine lifetimes matters when the work is no longer useful to the user.
Key terms: structured concurrency, hierarchy of coroutines, lifetimes, resource leaks, unnecessary work
15.1 Coroutine scopes establish structure between coroutines
Explains how coroutine scopes give launched coroutines parent-child relationships and let a parent wait for its children without tracking them manually.
- Coroutine scope parent-child relationships: Explain that each coroutine belongs to a coroutine scope and that calling launch or async inside another coroutine builder automatically makes the new coroutine a child of that coroutine.
Why it matters: This relationship is what lets Kotlin treat related coroutines as a group rather than as unrelated background work. - Parent waits for child coroutines: Describe why runBlocking can finish executing its own body yet keep the program running until its child coroutines finish, without manually tracking or awaiting them.
Why it matters: It explains why finishing a parent's code does not leave its child coroutines unfinished or require manual bookkeeping.
Key terms: structured concurrency, coroutine scope, CoroutineScope, launch, async, parent-child relationship, runBlocking, child coroutines
15.1.1 Creating a coroutine scope: The coroutineScope function
This subsection introduces coroutineScope as a way to group child coroutines for concurrent work and wait for them to finish.
- Coroutine scope completion: Explain that coroutineScope is a suspending function that creates a scope without creating a new coroutine, and completes only after all its child coroutines finish.
Why it matters: It explains how concurrent child work can be grouped into a computation that waits for its children. - Concurrent sum computation: Describe how the example uses coroutineScope to combine concurrently generated numbers, return their sum from the block, and why computeSum must be suspending.
Why it matters: The sum example shows that a scope can produce a result from concurrent work, not just launch children.
Key terms: coroutineScope, child coroutines, suspending function, concurrent decomposition of work, computeSum, suspending
15.1.2 Associating coroutine scopes with components: CoroutineScope
This section explains how a class can own a coroutine scope tied to its life cycle and distinguishes that use from suspending work decomposition.
- Two coroutine scope functions: Distinguish the suspending coroutineScope function from the CoroutineScope constructor function by what each is for and whether it waits.
Why it matters: The similar names conceal an important difference between waiting for work and owning a scope for later use. - Component scope life cycle: Describe how a component uses its own CoroutineScope to start coroutines and end them when the component stops.
Why it matters: A retained scope gives component-owned coroutines a clear point at which they are stopped. - Scope context and job choice: Explain what the CoroutineScope constructor takes and the stated reason for commonly supplying a SupervisorJob.
Why it matters: The context and job choice affect how coroutines in a component’s scope run and respond to an uncaught exception.
Key terms: coroutineScope, suspending function, life cycle, start, stop, cancel, context, dispatcher, Job, SupervisorJob
15.1.3 The danger of GlobalScope
This section explains why launching coroutines in GlobalScope breaks structured concurrency and why application code should usually use a more appropriate scope.
- GlobalScope and coroutine hierarchy: Explain why coroutines launched in GlobalScope are not children of the coroutine that launches them, and describe the resulting behavior in the runBlocking example.
Why it matters: The example shows how losing the parent–child relationship can make a program finish before its coroutines do. - GlobalScope lifecycle and scope choice: Explain the risks of using GlobalScope in application code and describe the section’s advice on choosing a scope instead.
Why it matters: Choosing an appropriate scope helps keep coroutine work tied to when it is needed.
Key terms: GlobalScope, structured concurrency, runBlocking, parent, children, life cycle, resource leaks, @DelicateCoroutinesApi, coroutineScope
15.1.4 Coroutine contexts and structured concurrency
Explains how coroutine contexts and Jobs follow the parent-child structure of coroutines, including how a child gets its dispatcher.
- Child coroutine context construction: Trace how a new coroutine receives its context: it inherits the parent context, creates a new Job linked to the parent's Job, and then applies any supplied context arguments, which can override inherited elements.
Why it matters: This explains both the relationship between coroutines and why a child may run with different context settings. - Inherited dispatcher selection: Explain which dispatcher a new coroutine uses when none is specified and distinguish that from assuming it always uses Dispatchers.Default.
Why it matters: It lets you predict where a child coroutine runs without assuming a universal default. - Visible Job hierarchy: Describe how to inspect the Job relationships between coroutines and state that coroutineScope also has a Job in that hierarchy.
Why it matters: The Job properties make the coroutine hierarchy something you can inspect rather than just infer.
Key terms: coroutine context, Job, parent-child relationship, Dispatcher, Dispatchers.Default, parent dispatcher, job.parent, job.children, coroutineScope, coroutineContext.job
15.2 Cancellation
Defines cancellation and explains why stopping unneeded coroutine work matters for efficiency, resource use, and error handling.
- Cancellation and unnecessary work: Explain that cancellation stops code before normal completion and prevents work whose result is no longer needed, such as a request after a user navigates away.
Why it matters: Stopping work that no longer has a purpose helps applications remain efficient. - Resource retention without cancellation: Explain how uncancelled work can retain resources or references to data that the application no longer needs.
Why it matters: Cancellation can prevent unneeded coroutines from tying up resources and memory. - Cancellation after dependent failure: Describe why a failed request can make cancellation useful when several coroutines are working together on one result.
Why it matters: Recognizing when a shared result is impossible helps avoid spending resources on the remaining work.
Key terms: cancellation, unnecessary work, resource leaks, rogue coroutines, garbage collector, asynchronous network requests
15.2.1 Triggering cancellation
This subsection explains how to trigger cancellation using the handles returned by coroutine builders or the Job in a coroutine scope’s context.
- Coroutine cancellation handles: Explain how to trigger cancellation of a coroutine created with launch or async, and how to cancel a coroutine scope.
Why it matters: These handles give you a way to request cancellation of a coroutine or a whole scope.
Key terms: launch, async, Job, Deferred, cancel, coroutine context
15.2.2 Invoking cancellation automatically after a time limit has been exceeded
This subsection shows how to limit a coroutine computation’s execution time and how the two timeout functions report an expired limit.
- Time-limited coroutine computations: Explain that withTimeout and withTimeoutOrNull constrain how long a computation may run: an expired timeout cancels the computation, while one that finishes in time produces its value.
Why it matters: A timeout lets you prevent a suspending computation from taking longer than you allow. - Timeout result and exception handling: Distinguish how withTimeout and withTimeoutOrNull report an expired timeout, and explain why an uncaught exception from withTimeout can be undesirable.
Why it matters: Choosing the null-returning form, or catching the exception, helps avoid accidentally cancelling the invoking coroutine.
Key terms: withTimeout, withTimeoutOrNull, timeout, cancellation, TimeoutCancellationException, try, catch
15.2.3 Cancellation cascades through all children
This subsection explains that cancelling a coroutine also cancels all its descendants, preventing child coroutines from continuing unnecessary work.
- Cascading child coroutine cancellation: Explain that cancelling a coroutine cancels all its child coroutines, including descendants several levels down, so they do not keep running after their parent is cancelled.
Why it matters: You can cancel an outer coroutine without leaving its descendants running unnecessarily.
Key terms: structured concurrency, child coroutines, cancellation
15.2.4 Cancelled coroutines throw CancellationExceptions in special places
Explains where cancellation is delivered to a coroutine and why catching its exception can prevent cancellation.
- Cancellation at suspension points: Explain that cancellation is delivered by throwing a CancellationException, primarily at suspension points, and relate this to the possible output of the A/B/C example.
Why it matters: It explains why a cancellation request may not interrupt a stretch of code immediately. - Preserving cancellation exceptions: Explain why a broad catch can prevent cancellation and how to change the catch so cancellation proceeds.
Why it matters: A catch meant for an ordinary error can accidentally keep cancelled work running.
Key terms: CancellationException, suspension points, suspending functions, withTimeoutOrNull, catch, UnsupportedOperationException
15.2.5 Cancellation is cooperative
Explains why a suspend function doing CPU-heavy work may ignore cancellation and how cancellable calls let it respond.
- Cooperative cancellation in suspending code: Explain why marking CPU-heavy work as suspend does not make it cancellable, and how calling a cancellable function changes that.
Why it matters: A suspend modifier alone will not let long-running work respond to cancellation.
Key terms: cooperative, suspend modifier, suspension points, CancellationException, delay
15.2.6 Checking whether a coroutine has been cancelled
This subsection shows how to check whether a coroutine has been cancelled and how to respond to that check.
- Coroutine cancellation status checks: Distinguish checking a CoroutineScope’s isActive property from calling ensureActive: a false isActive value lets the coroutine finish work, release resources, and return, while ensureActive throws a CancellationException when the coroutine is no longer active.
Why it matters: These checks let your code respond deliberately when its coroutine is no longer active.
Key terms: CoroutineScope, isActive, ensureActive, CancellationException
15.2.7 Letting other coroutines play: The yield function
Explains how yield adds a cancellation point and gives other coroutines a chance to run on an occupied dispatcher.
- Yield and coroutine interleaving: Explain why CPU-heavy coroutines without suspension points may run one after another, and how yield changes that behavior while also allowing cancellation.
Why it matters: Yield can keep CPU-heavy coroutine work from monopolizing a dispatcher while providing a place for cancellation to take effect.
Key terms: yield, suspension points, isActive, ensureActive, CancellationException, dispatcher
15.2.8 Keep cancellation in mind when acquiring resources
Explains why coroutine cancellation can leak resources and how to ensure they are closed.
- Resource cleanup during cancellation: Explain how cancellation can leave a coroutine’s resource open, and describe how a finally block or, for an AutoClosable resource, the use function ensures cleanup.
Why it matters: A cancelled coroutine should not leave a database connection or other resource open.
Key terms: cancellation, CancellationException, finally block, AutoClosable, use
15.2.9 Frameworks can perform cancellation for you
Frameworks can provide and cancel coroutine scopes, so coroutines should be launched in a scope whose lifetime matches the work they need to do.
- Framework-managed scope lifetimes: Explain how frameworks can provide coroutine scopes and cancel them when the associated component ends. Describe how Android's viewModelScope is cancelled when its ViewModel is cleared, along with coroutines launched in it.
Why it matters: Choosing a scope tied to a component lets its work stop when that component is no longer needed. - Ktor request and application scopes: Distinguish Ktor's request-level scope from its application scope, and explain which to choose for work that should stop on client disconnection versus work that should continue independently of the request.
Why it matters: Scope choice determines whether server work stops with an abandoned request or keeps running as intended.
Key terms: coroutine scope, viewModelScope, ViewModel, cancellation, PipelineContext, CoroutineScope, request-level coroutine scope, Application, application coroutine scope
15.3 Summary
Where this is used later
- Shared flows broadcast values to subscribers (chapter 16) · Uses a coroutine scope to structure the lifetime of the sharing coroutine.
- Error propagation in Kotlin coroutines (chapter 18) · The parent–child structure established by coroutine scopes.
- Concurrent flows with channel flows (chapter 16) · Using a coroutine scope to launch coroutines
- Supervisors prevent parents and siblings from being cancelled (chapter 18) · Associating coroutine scopes with components.
- CoroutineExceptionHandler: The last resort for processing exceptions (chapter 18) · Creating a coroutine scope with a SupervisorJob.
- Cancelling the collection of a flow (chapter 16) · Assumes the coroutine cancellation mechanisms introduced earlier.
- Coroutines cancel all their children when one child fails (chapter 18) · Builds on the reason for stopping related work when a child coroutine fails.
- Structured concurrency only affects exceptions thrown across coroutine boundaries (chapter 18) · Introduces the special role of CancellationExceptions in cancelled coroutines.
Concepts explained in this chapter
- task cancellationTask cancellation is a request or mechanism for stopping an asynchronous or in-progress operation before it completes normally.
- coroutine scopecoroutineScope is a kotlinx.coroutines suspending function that creates a nested coroutine scope, waits for its child coroutines, and returns the block's result.
- structured concurrencyStructured concurrency is a concurrency model that scopes concurrent tasks so their lifetimes and failures are managed through a shared parent-child hierarchy.
- SupervisorJobSupervisorJob is a coroutine Job implementation that prevents failure of one child coroutine from automatically cancelling its sibling coroutines.
- CancellationExceptionCancellationException is an exception used by Kotlin coroutines to signal that a coroutine has been cancelled.
- coroutine contextcoroutineContext is a compiler-provided property that exposes the context of the currently executing coroutine from a suspending function.
- coroutine hierarchyA coroutine hierarchy is a parent-child organization of coroutines through which execution lifetimes and failures can be related.
- asyncasync is a coroutine builder that starts an asynchronous computation and produces a handle for obtaining its result.
- child coroutineA child coroutine is a coroutine launched within the scope of another coroutine and associated with that parent coroutine.
- launchlaunch is a kotlinx.coroutines coroutine builder that starts a coroutine whose computation does not produce a result value.
- runBlockingrunBlocking is a kotlinx.coroutines coroutine builder that blocks the current thread while executing a coroutine and its suspending operations.
- suspension pointA suspension point is a point at which a suspending coroutine can pause and allow other coroutines sharing its dispatcher to execute.
- finallyA finally block is an exception-handling block that executes after a protected block regardless of whether an exception was thrown.
- resource leakA resource leak is the unintended retention or failure to release of resources such as threads, memory, or active concurrent tasks.
- JobJob is a coroutine-context element that represents the lifecycle of a coroutine and propagates child failure to its parent under regular structured concurrency.
- futureA future is an abstraction representing a result that may become available after an asynchronous computation finishes.
- coroutine builder functionA coroutine builder function is an API function that creates and starts a coroutine with specified execution, context, and result behavior.
- exceptionException is the Java base class for conditions that a program may catch and handle, excluding serious errors represented by Error.
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.
