Advanced Concurrency & Asynchrony

structured concurrency and the task scope

Think of how a function call already works: if main() calls helper(), helper() must finish before main() continues past that line. Control flow has clean nesting — children complete inside their parent. Now notice that plain 'go spawn a background task' breaks this: you fire off concurrent work and your function returns, leaving an orphan task running loose with no one responsible for it. Structured concurrency restores the nesting for concurrent tasks: every task is spawned inside a scope, and the scope does not exit until all tasks spawned within it have finished.

The mechanism is a task scope — also called a nursery (Trio's term) or a task group. You open a scope, spawn one or many concurrent tasks inside it, and at the closing boundary of the scope the code waits for all of them to complete before proceeding. Because the scope is a lexical block, the lifetime of the concurrent tasks is bounded by it: no task can outlive the block that created it. Three properties fall out automatically. Tasks cannot leak — they are joined at the scope's end. Errors propagate — if one child task fails, the scope can cancel its siblings and re-raise the error to the parent, instead of the failure vanishing in a detached task. And cancellation is scoped — cancelling the parent cancels everything spawned beneath it, recursively.

Why this is a big deal: unstructured spawning gives concurrency the same problems goto gave control flow — tasks that start somewhere and finish nowhere in particular, errors swallowed silently, resources held by tasks no one is tracking. Structured concurrency makes concurrent lifetimes visible in the code's shape, the way braces make call lifetimes visible. It is the organising idea behind Trio's nurseries, Kotlin coroutine scopes, Java's StructuredTaskScope, and Swift's task groups. The honest caveat: it deliberately trades some flexibility — a truly fire-and-forget background task that should outlive its creator does not fit the model, and you must step outside it (or pass a longer-lived scope) on purpose.

Trio-style nursery: async with open_nursery() as n: n.start_soon(fetch_a); n.start_soon(fetch_b); # the 'with' block does not exit until BOTH finish; if one raises, the other is cancelled and the error propagates here.

The scope joins all children at its boundary; no task escapes the block, and one child's failure can cancel its siblings.

Structured concurrency does not make tasks run in order or remove the need for synchronisation on shared data — concurrent tasks in a scope still race. It governs their lifetimes, error propagation, and cancellation, not their access to shared memory.

Also called
nurserytask scopescoped tasks保育室任務範圍