diff --git a/doc/.pa11yci.json b/doc/.pa11yci.json index c208cc99d..7895d467b 100644 --- a/doc/.pa11yci.json +++ b/doc/.pa11yci.json @@ -14,7 +14,7 @@ "http://localhost:8088/capy/index.html", "http://localhost:8088/capy/why-capy.html", "http://localhost:8088/capy/quick-start.html", - "http://localhost:8088/capy/4.coroutines/4a.tasks.html", + "http://localhost:8088/capy/4.coroutines/4b.tasks.html", "http://localhost:8088/capy/reference/boost/capy.html" ] } diff --git a/doc/lint/baseline.json b/doc/lint/baseline.json index 9c6257b53..932510712 100644 --- a/doc/lint/baseline.json +++ b/doc/lint/baseline.json @@ -11,27 +11,27 @@ "modules/ROOT/pages/3.concurrency/3b.synchronization.adoc:#1:Google.Colons", "modules/ROOT/pages/3.concurrency/3c.advanced.adoc:#1:Vale.Spelling", "modules/ROOT/pages/4.coroutines/4.intro.adoc:#1:Google.OxfordComma", - "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4a.tasks.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4b.launching.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4c.executors.adoc:#3:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Capy.NoFluff", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#2:Capy.NoFluff", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#3:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#4:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4e.cancellation.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#3:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4f.composition.adoc:#4:Vale.Spelling", - "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Google.LyHyphens", - "modules/ROOT/pages/4.coroutines/4g.allocators.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.tasks.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4b.tasks.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.launching.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4c.launching.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.executors.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.executors.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4d.executors.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#1:Capy.NoFluff", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#2:Capy.NoFluff", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.cancellation.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4f.cancellation.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#1:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#4:Vale.Spelling", + "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Google.LyHyphens", + "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Vale.Spelling", "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Google.LyHyphens", "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Google.OxfordComma", "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Vale.Spelling", @@ -402,7 +402,7 @@ "C2:lint/.docstrings/concept/io_awaitable.hpp.adoc:#4:sentence over 25 words", "C2:modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#1:sentence over 25 words", "C2:modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc:#2:sentence over 25 words", - "C2:modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc:#1:sentence over 25 words", + "C2:modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc:#1:sentence over 25 words", "advisory-C2:modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#1:sentence over 25 words", "advisory-C2:modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#2:sentence over 25 words", "advisory-C2:modules/ROOT/pages/9.design/9a.CapyLayering.adoc:#3:sentence over 25 words", @@ -514,25 +514,25 @@ "skipped": false, "contrastCount": 6, "fingerprints": [ - "/capy/4.coroutines/4a.tasks.html:color-contrast:#content > article > div:nth-child(1) > nav > ul > li:nth-child(2) > a", - "/capy/4.coroutines/4a.tasks.html:color-contrast:#content > article > div:nth-child(1) > nav > ul > li:nth-child(3) > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_awaiting_other_tasks > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_declaring_task_coroutines > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_exception_propagation > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_lazy_execution > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_move_semantics > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_overview > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_returning_values_with_co_return > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_running_a_task > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#_symmetric_transfer > a", - "/capy/4.coroutines/4a.tasks.html:link-name:#io-result-and-io-task > a", - "/capy/4.coroutines/4a.tasks.html:list:#toc > aside > div > div > nav > ul", - "/capy/4.coroutines/4a.tasks.html:list:#toc > aside > div > div > nav > ul > ul", - "/capy/4.coroutines/4a.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(10) > div > div:nth-child(2) > div > pre", - "/capy/4.coroutines/4a.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(11) > div > div:nth-child(5) > div > pre", - "/capy/4.coroutines/4a.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(12) > div > div:nth-child(2) > div > pre", - "/capy/4.coroutines/4a.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(7) > div > div:nth-child(3) > div > pre", - "/capy/4.coroutines/4a.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(8) > div > div:nth-child(2) > div > pre", + "/capy/4.coroutines/4b.tasks.html:color-contrast:#content > article > div:nth-child(1) > nav > ul > li:nth-child(2) > a", + "/capy/4.coroutines/4b.tasks.html:color-contrast:#content > article > div:nth-child(1) > nav > ul > li:nth-child(3) > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_awaiting_other_tasks > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_declaring_task_coroutines > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_exception_propagation > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_lazy_execution > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_move_semantics > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_overview > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_returning_values_with_co_return > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_running_a_task > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#_symmetric_transfer > a", + "/capy/4.coroutines/4b.tasks.html:link-name:#io-result-and-io-task > a", + "/capy/4.coroutines/4b.tasks.html:list:#toc > aside > div > div > nav > ul", + "/capy/4.coroutines/4b.tasks.html:list:#toc > aside > div > div > nav > ul > ul", + "/capy/4.coroutines/4b.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(10) > div > div:nth-child(2) > div > pre", + "/capy/4.coroutines/4b.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(11) > div > div:nth-child(5) > div > pre", + "/capy/4.coroutines/4b.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(12) > div > div:nth-child(2) > div > pre", + "/capy/4.coroutines/4b.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(7) > div > div:nth-child(3) > div > pre", + "/capy/4.coroutines/4b.tasks.html:scrollable-region-focusable:#content > article > div:nth-child(8) > div > div:nth-child(2) > div > pre", "/capy/index.html:color-contrast:#content > article > div:nth-child(1) > nav > ul > li:nth-child(2) > a", "/capy/index.html:link-name:#_assumed_knowledge > a", "/capy/index.html:link-name:#_code_convention > a", diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 9a4073f82..52812b1d8 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -12,14 +12,15 @@ ** xref:3.concurrency/3c.advanced.adoc[Atomics, Condition Variables & Shared Locks] ** xref:3.concurrency/3d.patterns.adoc[Futures, async & Patterns] * xref:4.coroutines/4.intro.adoc[Coroutines in Capy] -** xref:4.coroutines/4a.tasks.adoc[The task Type] -** xref:4.coroutines/4b.launching.adoc[Starting Coroutines] -** xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] -** xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -** xref:4.coroutines/4e.cancellation.adoc[Stop Tokens and Cancellation] -** xref:4.coroutines/4f.composition.adoc[Concurrent Composition] -** xref:4.coroutines/4g.allocators.adoc[Frame Allocators] -** xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] +** xref:4.coroutines/4a.flow-control.adoc[Flow control] +** xref:4.coroutines/4b.tasks.adoc[The task Type] +** xref:4.coroutines/4c.launching.adoc[Starting Coroutines] +** xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] +** xref:4.coroutines/4e.io-awaitable.adoc[The IoAwaitable Protocol] +** xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] +** xref:4.coroutines/4g.composition.adoc[Concurrent Composition] +** xref:4.coroutines/4h.allocators.adoc[Frame Allocators] +** xref:4.coroutines/4i.lambda-captures.adoc[Lambda Coroutine Captures] * xref:5.buffers/5a.buffers.adoc[Buffer Sequences] * xref:6.streams/6.intro.adoc[Stream Concepts] ** xref:6.streams/6a.overview.adoc[Overview] diff --git a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc index 00b48844a..af9403e67 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc @@ -14,19 +14,21 @@ Capy's coroutine model is built around a single principle: asynchronous code sho == What This Section Covers -* xref:4.coroutines/4a.tasks.adoc[The task Type] -- Declaring, returning values from, and +* xref:4.coroutines/4a.flow-control.adoc[Flow control] -- Overview of how coroutines + address the needs of asynchronous I/O in Capy. +* xref:4.coroutines/4b.tasks.adoc[The task Type] -- Declaring, returning values from, and awaiting cpp:task[task] coroutines. -* xref:4.coroutines/4b.launching.adoc[Starting Coroutines] -- Starting coroutines with +* xref:4.coroutines/4c.launching.adoc[Starting Coroutines] -- Starting coroutines with cpp:run_async[], and binding child tasks with cpp:run[]. -* xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] -- Executors, +* xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] -- Executors, execution contexts, thread pools, and strands. -* xref:4.coroutines/4d.io-awaitable.adoc[The IoAwaitable Protocol] -- How the executor and +* xref:4.coroutines/4e.io-awaitable.adoc[The IoAwaitable Protocol] -- How the executor and stop token propagate through a chain of awaited coroutines. -* xref:4.coroutines/4e.cancellation.adoc[Stop Tokens and Cancellation] -- Cooperative +* xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] -- Cooperative cancellation with `std::stop_token`, and how Capy tasks observe it. -* xref:4.coroutines/4f.composition.adoc[Concurrent Composition] -- Running tasks +* xref:4.coroutines/4g.composition.adoc[Concurrent Composition] -- Running tasks concurrently with cpp:when_all[] and cpp:when_any[]. -* xref:4.coroutines/4g.allocators.adoc[Frame Allocators] -- How coroutine frames are +* xref:4.coroutines/4h.allocators.adoc[Frame Allocators] -- How coroutine frames are allocated, and how to customize the allocator. -* xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] -- A critical +* xref:4.coroutines/4i.lambda-captures.adoc[Lambda Coroutine Captures] -- A critical pitfall: lambda captures versus coroutine frame lifetime. diff --git a/doc/modules/ROOT/pages/4.coroutines/4a.flow-control.adoc b/doc/modules/ROOT/pages/4.coroutines/4a.flow-control.adoc new file mode 100644 index 000000000..9c496f270 --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4a.flow-control.adoc @@ -0,0 +1,36 @@ += Flow control +:page-mode: explanation + +Capy's design is driven by the needs of network I/O processing, +where a piece of the program, in order to continue, needs to wait for an I/O operation to finish, +but when this happens is unpredictable. The challenge here is: + + 1. To utilize this waiting time as efficiently as possible, processing other tasks. + 2. To avoid any concurrency or lifetime management bugs. + 3. To have the user code be simple and intuitive. + +To achieve this, Capy utilizes the _proactor_ pattern hidden behind the coroutine `co_await` mechanism. +In this pattern, when the user needs an I/O operation to be performed to see its results, they: + + 1. Schedule in the I/O runtime an operation to be performed. + 2. Register a piece of code to be invoked when the I/O operation finishes. + 3. Return control, so that other tasks can make progress. + +This is exactly what happens when you write: + +[source,cpp] +---- +process(co_await stream.read_some(buffer)); + /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ <1> +/*~~~*/ <2> +---- +<1> The `co_await`-expression schedules an I/O operation to be called in the + xref:4.coroutines/4d.executors.adoc[_execution context_], + and registers + the resumption of the coroutine after the I/O operation finishes. Once the scheduling and registering is done, + the coroutine is suspended, and the program thread switches to performing other tasks. +<2> When the xref:4.coroutines/4d.executors.adoc[_execution context_] finishes performing the scheduled I/O operation, it resumes the coroutine, + and the next instruction following the `co_await`-expression. + +While all this registering happens hidden in the coroutine mechanism, the coroutine body +reads as sequential business logic. \ No newline at end of file diff --git a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc similarity index 87% rename from doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc rename to doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc index baab2333f..9e039170c 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc @@ -5,7 +5,7 @@ cpp:task[task] is declared in: [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=include_task] +include::example$snippets/4b_tasks.cpp[tag=include_task] ---- [NOTE] @@ -14,7 +14,7 @@ include::example$snippets/4a_tasks.cpp[tag=include_task] [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=include_umbrella] +include::example$snippets/4b_tasks.cpp[tag=include_umbrella] ---- Unless otherwise specified, all code examples in this documentation assume the following: @@ -40,7 +40,7 @@ Key characteristics: * *Symmetric transfer* — Efficient resumption without stack accumulation * *Executor inheritance* — Inherits the caller's executor unless explicitly bound * *Stop token propagation* — Forward-propagates cancellation signals -* *xref:4.coroutines/4g.allocators.adoc#_halo_optimization[HALO] support* — Enables Heap Allocation eLision Optimization when possible +* *xref:4.coroutines/4h.allocators.adoc#_halo_optimization[HALO] support* — Enables Heap Allocation eLision Optimization when possible == Declaring task Coroutines @@ -48,7 +48,7 @@ Any function that returns cpp:task[task] and contains coroutine keywords (`co [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=declaring] +include::example$snippets/4b_tasks.cpp[tag=declaring] ---- The syntax cpp:task[task<>] is equivalent to `task`. @@ -59,7 +59,7 @@ Use `co_return` to complete the coroutine and provide its result: [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=returning] +include::example$snippets/4b_tasks.cpp[tag=returning] ---- For cpp:task[task], you can either use `co_return;` explicitly or let execution fall off the end of the function body. @@ -73,14 +73,14 @@ cpp:io_task[io_task] names a task returning one of those results: [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=io_task] +include::example$snippets/4b_tasks.cpp[tag=io_task] ---- Because cpp:io_result[] is a `std::tuple`, the whole standard tuple API applies: structured bindings, `std::tie`, `std::apply`, `std::get`, `std::tuple_cat`, comparisons, and tuple assignment. The error code is the first element, so `auto [ec, n] = co_await s.read_some(buf);` destructures it directly, and `std::tie` rebinds into existing variables without introducing new ones. Always test the error code before reading a payload; a payload's meaning when it is set is defined by the operation that produced it. For cpp:io_result[io_result<>] -- no payload -- a `std::error_code` converts implicitly, so `co_return some_ec;` compiles. With payloads present you must supply the whole result, as `count_ready` shows. -These two names are the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4f.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Stream Concepts] both assume them. +These two names are the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4g.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Stream Concepts] both assume them. == Running a Task @@ -88,7 +88,7 @@ A cpp:task[task] is lazy, so declaring one does not run it. To run a task to com [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=run] +include::example$snippets/4b_tasks.cpp[tag=run] ---- Here `add(2, 3)` runs on a cpp:thread_pool[] executor, and the completion handler receives the result, `5`. The call to `pool.join()` waits for the pooled work to finish before the result is read. @@ -99,7 +99,7 @@ Tasks can await other tasks using `co_await`. This is the primary mechanism for [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=awaiting] +include::example$snippets/4b_tasks.cpp[tag=awaiting] ---- When you `co_await` a task: @@ -115,7 +115,7 @@ A critical property of cpp:task[task] is *lazy execution*: creating a task do [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=lazy] +include::example$snippets/4b_tasks.cpp[tag=lazy] ---- *Output:* @@ -135,7 +135,7 @@ When a task completes, control transfers directly to its continuation (the corou [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=chain] +include::example$snippets/4b_tasks.cpp[tag=chain] ---- Without symmetric transfer, each `co_await` would add a stack frame, potentially causing stack overflow with deep nesting. With symmetric transfer, `c` returning to `b` returning to `a` uses constant stack space regardless of depth. @@ -144,7 +144,7 @@ This is implemented through the `await_suspend` returning a coroutine handle rat [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=final_suspend,indent=0] +include::example$snippets/4b_tasks.cpp[tag=final_suspend,indent=0] ---- == Move Semantics @@ -153,7 +153,7 @@ Tasks are move-only. Copying a task would create aliasing problems where multipl [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=move_only] +include::example$snippets/4b_tasks.cpp[tag=move_only] ---- After moving, the source task becomes empty and must not be awaited. @@ -164,7 +164,7 @@ Exceptions thrown inside a task are captured and rethrown when the task is await [source,cpp] ---- -include::example$snippets/4a_tasks.cpp[tag=exceptions] +include::example$snippets/4b_tasks.cpp[tag=exceptions] ---- The exception is stored in the promise when it occurs and rethrown in `await_resume` when the calling coroutine resumes. diff --git a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc b/doc/modules/ROOT/pages/4.coroutines/4c.launching.adoc similarity index 89% rename from doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc rename to doc/modules/ROOT/pages/4.coroutines/4c.launching.adoc index 854aa1e7f..9e97774b1 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4c.launching.adoc @@ -16,7 +16,7 @@ cpp:run_async[] takes an executor, creates the necessary context, and starts the [source,cpp] ---- -include::example$programs/4b_launching_run_async.cpp[tag=full] +include::example$programs/4c_launching_run_async.cpp[tag=full] ---- === Two-Call Syntax @@ -60,7 +60,7 @@ argument to the helper, which is before the helper's body runs. The two silent patterns produce no diagnostic at all: the task runs, on a coroutine frame that came from the wrong allocator. cpp:run_async_wrapper[] documents each pattern, and -xref:4.coroutines/4g.allocators.adoc#two-call-rationale[Frame Allocators] carries the +xref:4.coroutines/4h.allocators.adoc#two-call-rationale[Frame Allocators] carries the {cpp}17-evaluation-order rationale. Always use the two-call pattern in a single expression. @@ -72,7 +72,7 @@ cpp:run_async[] accepts optional handlers for results and exceptions: [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=handlers,indent=0] +include::example$snippets/4c_launching.cpp[tag=handlers,indent=0] ---- When no result handler is provided, the result is discarded. An exception @@ -88,7 +88,7 @@ Inside a coroutine, use cpp:run[] to execute a child task on a different executo [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=run_hop] +include::example$snippets/4c_launching.cpp[tag=run_hop] ---- === Executor Affinity @@ -113,7 +113,7 @@ Since cpp:run_async[] is called from non-coroutine code, there is no caller toke [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=inject_token,indent=0] +include::example$snippets/4c_launching.cpp[tag=inject_token,indent=0] ---- === Inheritance with run @@ -122,14 +122,14 @@ cpp:run[] is called from within a coroutine, so it inherits the caller's stop to [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=inherit_token,indent=0] +include::example$snippets/4c_launching.cpp[tag=inherit_token,indent=0] ---- To override with a different token, pass it explicitly: [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=override_token,indent=0] +include::example$snippets/4c_launching.cpp[tag=override_token,indent=0] ---- == Handler Threading @@ -138,6 +138,6 @@ Handlers passed to cpp:run_async[] are invoked on whatever thread the executor s [source,cpp] ---- -include::example$snippets/4b_launching.cpp[tag=handler_thread,indent=0] +include::example$snippets/4c_launching.cpp[tag=handler_thread,indent=0] ---- diff --git a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc b/doc/modules/ROOT/pages/4.coroutines/4d.executors.adoc similarity index 89% rename from doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc rename to doc/modules/ROOT/pages/4.coroutines/4d.executors.adoc index d2b7fdc1f..98eda5afc 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4d.executors.adoc @@ -12,20 +12,20 @@ This rule is what keeps shared state safe by default. Consider a connection hand [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=handle_client] +include::example$snippets/4d_executors.cpp[tag=handle_client] ---- Start this on a strand, and every resumption—after `conn.read()` and after `conn.write()`—happens on that strand. The update to `conn.stats.requests` is therefore free of data races without any mutex. Without the invariant, the coroutine could resume after `co_await conn.read()` on an `io_uring` completion thread, a pool thread, or wherever the I/O subsystem completed the operation. Correct code would then need either a mutex around every access to shared state or an explicit _resume-on-this-strand_ step after every `co_await`. A mutex defeats the purpose of the strand, and a single forgotten resume step reintroduces the data race. -Because the invariant holds, the safe behavior is automatic and the unsafe behavior does not compile. Awaiting a _plain_ awaitable—one that could resume the coroutine on any thread—is rejected. See xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] for why, and for the explicit escape hatch. +Because the invariant holds, the safe behavior is automatic and the unsafe behavior does not compile. Awaiting a _plain_ awaitable—one that could resume the coroutine on any thread—is rejected. See xref:4.coroutines/4e.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] for why, and for the explicit escape hatch. === How the Invariant Is Maintained Affinity propagates forward. Starting a task with cpp:run_async[]`(ex)` binds it to `ex`. A child `co_await`-ed from that task inherits the same executor automatically, and so on down the chain. When a child completes, control returns to its caller _through the caller's executor_. When both share the same executor—the common case—that return is a direct symmetric transfer with no queuing. Only a deliberate executor change requires a dispatch. -That deliberate change is what `run` provides. It runs a subtree on a different executor and restores the caller's executor when the subtree completes (see xref:4.coroutines/4b.launching.adoc[Starting Coroutines]). This is also why I/O objects with executor-bound invariants—a socket tied to one `io_context`, a Windows IOCP handle, executor-specific timer state—remain safe. A coroutine holding them never resumes on the wrong executor mid-body. +That deliberate change is what `run` provides. It runs a subtree on a different executor and restores the caller's executor when the subtree completes (see xref:4.coroutines/4c.launching.adoc[Starting Coroutines]). This is also why I/O objects with executor-bound invariants—a socket tied to one `io_context`, a Windows IOCP handle, executor-specific timer state—remain safe. A coroutine holding them never resumes on the wrong executor mid-body. == The Executor Concept @@ -54,7 +54,7 @@ cpp:executor_ref[] wraps any executor in a type-erased container, allowing code [source,cpp] ---- -include::example$programs/4c_executors_executor_ref.cpp[tag=full] +include::example$programs/4d_executors_executor_ref.cpp[tag=full] ---- cpp:executor_ref[] stores a reference to the underlying executor—the original executor must outlive the `executor_ref`. @@ -65,7 +65,7 @@ cpp:thread_pool[] manages a pool of worker threads that execute coroutines concu [source,cpp] ---- -include::example$programs/4c_executors_thread_pool.cpp[tag=full] +include::example$programs/4d_executors_thread_pool.cpp[tag=full] ---- === Constructor Parameters @@ -90,7 +90,7 @@ Custom execution contexts inherit from cpp:execution_context[]: [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=my_context] +include::example$snippets/4d_executors.cpp[tag=my_context] ---- == strand: Serialization Without Mutexes @@ -99,7 +99,7 @@ A cpp:strand[] ensures that handlers are executed in order, with no two handlers [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=shared_resource] +include::example$snippets/4d_executors.cpp[tag=shared_resource] ---- === How Strands Work @@ -126,7 +126,7 @@ For single-threaded applications, use a context with one thread: [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=single_thread,indent=0] +include::example$snippets/4d_executors.cpp[tag=single_thread,indent=0] ---- === Multi-Threaded with Shared Data @@ -135,7 +135,7 @@ For multi-threaded applications with shared data, use strands: [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=data_strand,indent=0] +include::example$snippets/4d_executors.cpp[tag=data_strand,indent=0] ---- === Multi-Threaded with Independent Work @@ -144,6 +144,6 @@ For embarrassingly parallel work with no shared state: [source,cpp] ---- -include::example$snippets/4c_executors.cpp[tag=independent_tasks,indent=0] +include::example$snippets/4d_executors.cpp[tag=independent_tasks,indent=0] ---- diff --git a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc b/doc/modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc similarity index 92% rename from doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc rename to doc/modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc index 1cbf3fc81..365da976c 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4e.io-awaitable.adoc @@ -7,7 +7,7 @@ Standard {cpp}20 coroutines define awaiters with this `await_suspend` signature: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=std_await_suspend,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=std_await_suspend,indent=0] ---- The awaiter receives only a handle to the suspended coroutine. But real applications need more: @@ -42,7 +42,7 @@ the execution environment: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=two_arg_await_suspend,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=two_arg_await_suspend,indent=0] ---- This signature receives: @@ -97,7 +97,7 @@ When you write `co_await child_task()` inside a cpp:task[task]: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=context_flow,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=context_flow,indent=0] ---- The child receives the parent's executor and stop token automatically. @@ -124,7 +124,7 @@ To create a custom IoAwaitable: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=my_awaitable] +include::example$snippets/4e_io_awaitable.cpp[tag=my_awaitable] ---- The key points: @@ -146,17 +146,17 @@ Post the resume through the executor instead of resuming inline: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=stoppable_awaitable] +include::example$snippets/4e_io_awaitable.cpp[tag=stoppable_awaitable] ---- The incorrect pattern—which compiles and appears to work but causes memory corruption—looks like this: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=wrong_stop_callback,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=wrong_stop_callback,indent=0] ---- -See xref:4.coroutines/4e.cancellation.adoc#stoppable-awaitables[Implementing Stoppable Awaitables] for a complete example. +See xref:4.coroutines/4f.cancellation.adoc#stoppable-awaitables[Implementing Stoppable Awaitables] for a complete example. For a production implementation of this exact pattern, read the source of cpp:async_waker::wait_awaiter[]. It registers a stop callback that posts the resume through the executor, and arbitrates between wakeup and cancellation with a single atomic claim. @@ -166,14 +166,14 @@ This awaitable produces a value and resumes the caller by posting its cpp:contin [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=runnable_awaitable] +include::example$snippets/4e_io_awaitable.cpp[tag=runnable_awaitable] ---- To run that task from ordinary, non-coroutine code, hand it to cpp:run_async[] with an executor and a completion handler: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=run_awaitable,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=run_awaitable,indent=0] ---- The task runs on the cpp:thread_pool[] executor. When the awaitable posts its continuation, the pool resumes the task, `await_resume` returns the value, and it arrives at the completion handler—here, `5`. @@ -187,16 +187,16 @@ Capy rejects it at compile time: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=reject_plain,indent=0] +include::example$snippets/4e_io_awaitable.cpp[tag=reject_plain,indent=0] ---- -This is intentional. A plain awaitable receives only the coroutine handle; it can resume the coroutine on any thread by calling `handle.resume()` directly. That silently breaks the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant]—the coroutine could wake on a foreign completion thread, leaving shared state you believed was strand-protected exposed to races. Rejecting such an awaitable at compile time prevents that. The constraint does not lock you in; it requires environment propagation to be explicit rather than silently dropped. +This is intentional. A plain awaitable receives only the coroutine handle; it can resume the coroutine on any thread by calling `handle.resume()` directly. That silently breaks the xref:4.coroutines/4d.executors.adoc#the-same-executor-invariant[same-executor invariant]—the coroutine could wake on a foreign completion thread, leaving shared state you believed was strand-protected exposed to races. Rejecting such an awaitable at compile time prevents that. The constraint does not lock you in; it requires environment propagation to be explicit rather than silently dropped. The escape hatch is to wrap the foreign awaitable (or callback, or future) in a small cpp:IoAwaitable[]. That awaitable captures the executor and re-posts the resumption through it: [source,cpp] ---- -include::example$snippets/4d_io_awaitable.cpp[tag=foreign_bridge] +include::example$snippets/4e_io_awaitable.cpp[tag=foreign_bridge] ---- The single rule that makes any bridge correct: *on completion, post through `env->executor` instead of resuming the handle inline.* diff --git a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc b/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc similarity index 91% rename from doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc rename to doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc index 6aeff681e..42d68dc75 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc @@ -14,7 +14,7 @@ The obvious solution seems to be a boolean flag: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=naive_flag,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=naive_flag,indent=0] ---- This approach has problems: @@ -51,7 +51,7 @@ The *Observer Registration*. An RAII object that registers a callback to run whe [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=observer_pattern,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=observer_pattern,indent=0] ---- *Output:* @@ -70,7 +70,7 @@ If a callback is registered after `request_stop()` was already called, the callb [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=immediate_invocation,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=immediate_invocation,indent=0] ---- This ensures observers never miss the signal, regardless of registration timing. @@ -109,7 +109,7 @@ To "reset," create an entirely new `stop_source`: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=reset_workaround,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=reset_workaround,indent=0] ---- This is manual and error-prone. Any code still holding the old token does not receive new signals. @@ -141,7 +141,7 @@ Capy propagates stop tokens downward through `co_await`. When you await a task, [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=token_propagation,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=token_propagation,indent=0] ---- === Accessing the Stop Token @@ -150,7 +150,7 @@ Inside a task, use `co_await this_coro::stop_token` to access the current stop t [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=access_stop_token,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=access_stop_token,indent=0] ---- === Why Not `coroutine_handle::destroy()`? @@ -170,7 +170,7 @@ The rule: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=check_token,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=check_token,indent=0] ---- === Cleanup with RAII @@ -179,7 +179,7 @@ RAII ensures resources are released on early exit: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=raii_cleanup,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=raii_cleanup,indent=0] ---- === The canceled Convention @@ -188,7 +188,7 @@ When cancellation causes an operation to fail, the conventional error code is cp [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=canceled_convention,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=canceled_convention,indent=0] ---- == OS Integration @@ -208,7 +208,7 @@ A `std::stop_callback` fires synchronously on whatever thread calls `request_sto [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=inline_resume_wrong,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=inline_resume_wrong,indent=0] ---- When an external thread calls `request_stop()`, `h.resume()` executes the coroutine on that thread. The coroutine machinery sets the thread-local frame allocator to the executor's allocator—poisoning the calling thread's TLS. When the executor's pool destructs, the TLS pointer becomes dangling. The next coroutine allocation on that thread dereferences freed memory. @@ -221,7 +221,7 @@ Post the coroutine handle through the executor instead of resuming it inline. Th [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=stoppable_awaitable,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=stoppable_awaitable,indent=0] ---- NOTE: Capy's built-in I/O awaitables (via Corosio) already use the post-back pattern internally. This guidance applies when writing your own custom awaitables. @@ -236,7 +236,7 @@ of that race; a user thread supplies the clock: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=racing_deadline,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=racing_deadline,indent=0] ---- cpp:when_any[]`(await_fetch(ch), deadline(waker))` returns as soon as either side @@ -256,7 +256,7 @@ Connect UI cancellation to stop tokens. Pass the token through cpp:run_async[] s [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=user_cancellation,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=user_cancellation,indent=0] ---- === Graceful Shutdown @@ -265,12 +265,12 @@ Cancel all pending work during shutdown: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=graceful_shutdown,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=graceful_shutdown,indent=0] ---- === when_any Cancellation -cpp:when_any[] uses stop tokens internally to cancel "losing" tasks when the first task completes. This is covered in xref:4.coroutines/4f.composition.adoc[Concurrent Composition]. +cpp:when_any[] uses stop tokens internally to cancel "losing" tasks when the first task completes. This is covered in xref:4.coroutines/4g.composition.adoc[Concurrent Composition]. == The Standard Library Types @@ -278,7 +278,7 @@ The stop token mechanism is part of the {cpp} standard library, not Capy: [source,cpp] ---- -include::example$snippets/4e_cancellation.cpp[tag=include_stop_token,indent=0] +include::example$snippets/4f_cancellation.cpp[tag=include_stop_token,indent=0] ---- Key types: diff --git a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc similarity index 87% rename from doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc rename to doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc index e336469a2..e3fc07ac7 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -3,7 +3,7 @@ Headers: `` and ``. cpp:async_waker[] on this page is ``. -Both combinators take cpp:io_task[] children and return their results as a single cpp:io_result[]. If those two names are new, read xref:4.coroutines/4a.tasks.adoc#io-result-and-io-task[Reporting Errors] first -- everything below is expressed in them. +Both combinators take cpp:io_task[] children and return their results as a single cpp:io_result[]. If those two names are new, read xref:4.coroutines/4b.tasks.adoc#io-result-and-io-task[Reporting Errors] first -- everything below is expressed in them. == Overview @@ -11,14 +11,14 @@ Sequential execution—one task after another—is the default when using `co_aw [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=sequential,indent=0] +include::example$snippets/4g_composition.cpp[tag=sequential,indent=0] ---- For independent operations, concurrent execution is more efficient: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=concurrent,indent=0] +include::example$snippets/4g_composition.cpp[tag=concurrent,indent=0] ---- == when_all: Wait for All Tasks @@ -27,7 +27,7 @@ cpp:when_all[] starts multiple cpp:io_task[] children concurrently and waits for [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_all_basic,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_all_basic,indent=0] ---- === Result Type @@ -40,14 +40,14 @@ cpp:io_task[io_task<>] children contribute `tuple<>` to the result: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_all_void_mix,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_all_void_mix,indent=0] ---- When all children are cpp:io_task[io_task<>], just check the error code: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_all_all_void,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_all_all_void,indent=0] ---- === Error Handling @@ -60,14 +60,14 @@ I/O errors are reported through the error code that leads the cpp:io_result[io_r [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_all_error,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_all_error,indent=0] ---- If a task throws an exception, it is captured and rethrown after all tasks complete. Exceptions take priority over `ec`. [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_all_exception,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_all_exception,indent=0] ---- === Stop Propagation @@ -76,7 +76,7 @@ Well-behaved tasks should check their stop token and exit promptly: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=stop_propagation,indent=0] +include::example$snippets/4g_composition.cpp[tag=stop_propagation,indent=0] ---- == when_any: First-to-Succeed Wins @@ -85,7 +85,7 @@ cpp:when_any[] starts multiple cpp:io_task[] children concurrently and returns w [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=when_any_basic,indent=0] +include::example$snippets/4g_composition.cpp[tag=when_any_basic,indent=0] ---- The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. When a winner is found, stop is requested for all siblings. All tasks complete before cpp:when_any[] returns. @@ -106,14 +106,14 @@ The first pattern translates a specific, benign error into success. Other errors [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=wrap_translate_error,indent=0] +include::example$snippets/4g_composition.cpp[tag=wrap_translate_error,indent=0] ---- The second pattern lifts the inner `ec` into the payload. The wrapper always succeeds, so it wins on its first completion, carrying the original error code to the caller: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=wrap_lift_error,indent=0] +include::example$snippets/4g_composition.cpp[tag=wrap_lift_error,indent=0] ---- == Practical Patterns @@ -124,7 +124,7 @@ Fetch multiple resources simultaneously: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=parallel_fetch,indent=0] +include::example$snippets/4g_composition.cpp[tag=parallel_fetch,indent=0] ---- === Fan-Out/Fan-In @@ -133,7 +133,7 @@ Process items in parallel, then combine results using the range overload: [source,cpp] ---- -include::example$snippets/4f_composition.cpp[tag=fan_out,indent=0] +include::example$snippets/4g_composition.cpp[tag=fan_out,indent=0] ---- [NOTE] diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc b/doc/modules/ROOT/pages/4.coroutines/4h.allocators.adoc similarity index 92% rename from doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc rename to doc/modules/ROOT/pages/4.coroutines/4h.allocators.adoc index 5f22fd406..71d3c6339 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4h.allocators.adoc @@ -9,14 +9,14 @@ Pass an allocator to cpp:run_async[]: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=run_async_pmr_alloc,indent=0] +include::example$snippets/4h_allocators.cpp[tag=run_async_pmr_alloc,indent=0] ---- Or pass a `memory_resource*` directly: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=run_async_memory_resource,indent=0] +include::example$snippets/4h_allocators.cpp[tag=run_async_memory_resource,indent=0] ---- === Default Allocator @@ -53,14 +53,14 @@ This `memory_resource` pools each freed block by size and counts how many alloca [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=recycling_observe_resource,indent=0] +include::example$snippets/4h_allocators.cpp[tag=recycling_observe_resource,indent=0] ---- Run the same task eight times through it, one run at a time: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=recycling_observe,indent=0] +include::example$snippets/4h_allocators.cpp[tag=recycling_observe,indent=0] ---- The first run finds an empty pool and takes its coroutine frames from the heap. Each `pool.join()` returns those frames to the resource, so the next cpp:run_async[] call reuses them. Once the pool is warm, the upstream count stops climbing while the task keeps running -- the seven warm runs allocate nothing new. cpp:recycling_memory_resource[] does the same thing with size-class freelists and a lock-free thread-local cache. @@ -77,7 +77,7 @@ Capy's cpp:task[task] uses the `+[[clang::coro_await_elidable]]+` attribute ( [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=task_elidable] +include::example$snippets/4h_allocators.cpp[tag=task_elidable] ---- === When HALO Applies @@ -86,7 +86,7 @@ HALO is most effective for immediately-awaited tasks: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=halo_patterns,indent=0] +include::example$snippets/4h_allocators.cpp[tag=halo_patterns,indent=0] ---- === Measuring HALO Effectiveness @@ -109,7 +109,7 @@ When starting many short-lived tasks together, a monotonic buffer resource can b [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=batch_allocator] +include::example$snippets/4h_allocators.cpp[tag=batch_allocator] ---- === Scope Variables to Reduce Frame Size @@ -120,18 +120,18 @@ Wrapping buffer usage in explicit braces can therefore cut frame size sharply: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=frame_scope_bad] +include::example$snippets/4h_allocators.cpp[tag=frame_scope_bad] -include::example$snippets/4g_allocators.cpp[tag=frame_scope_good] +include::example$snippets/4h_allocators.cpp[tag=frame_scope_good] ---- The same technique lets Clang *overlap* two variables. When their lifetimes cannot coexist, both can occupy one frame offset: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_bad] +include::example$snippets/4h_allocators.cpp[tag=pipeline_overlap_bad] -include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_good] +include::example$snippets/4h_allocators.cpp[tag=pipeline_overlap_good] ---- In the second version, `read_buf` and `write_buf` never coexist, so they can share storage. This applies to any variables with non-overlapping lifetimes, not just arrays. @@ -202,11 +202,11 @@ This is why cpp:run_async[] uses two-call syntax: [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=two_call,indent=0] +include::example$snippets/4h_allocators.cpp[tag=two_call,indent=0] ---- Three patterns split those two calls apart, and two of them do it without any -diagnostic. xref:4.coroutines/4b.launching.adoc[Starting Coroutines] lists all +diagnostic. xref:4.coroutines/4c.launching.adoc[Starting Coroutines] lists all three. === The Window @@ -227,7 +227,7 @@ To prevent this, any code that calls `.resume()` on a coroutine handle must use [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=safe_resume,indent=0] +include::example$snippets/4h_allocators.cpp[tag=safe_resume,indent=0] ---- cpp:safe_resume[] saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (cpp:thread_pool[], strands, cpp:test::blocking_context[blocking_context]) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:8.examples/8n.custom-executor.adoc[Custom Executor] for an example. @@ -255,7 +255,7 @@ Most users never need to allocate coroutine frames manually -- cpp:task[task] [source,cpp] ---- -include::example$snippets/4g_allocators.cpp[tag=frame_alloc_mixin] +include::example$snippets/4h_allocators.cpp[tag=frame_alloc_mixin] ---- cpp:frame_alloc_mixin[] (in ``) supplies `operator new` and `operator delete` that: diff --git a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc b/doc/modules/ROOT/pages/4.coroutines/4i.lambda-captures.adoc similarity index 90% rename from doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc rename to doc/modules/ROOT/pages/4.coroutines/4i.lambda-captures.adoc index a510c2209..ba267a5c7 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4i.lambda-captures.adoc @@ -9,7 +9,7 @@ The lambda is defined and called in one expression -- the `()` after the closing [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=dangling_capture] +include::example$snippets/4i_lambda_captures.cpp[tag=dangling_capture] ---- **This code has undefined behavior.** It may crash, corrupt memory, or appear to work until it doesn't. @@ -36,7 +36,7 @@ The solution is to pass values as **function parameters** instead of **lambda ca [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=iife_parameter] +include::example$snippets/4i_lambda_captures.cpp[tag=iife_parameter] ---- The parameter `s` is copied to the coroutine frame before the first suspension, so it remains valid for the coroutine's lifetime. @@ -47,14 +47,14 @@ The parameter `s` is copied to the coroutine frame before the first suspension, [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=capture_this_broken] +include::example$snippets/4i_lambda_captures.cpp[tag=capture_this_broken] ---- === Correct: Using Parameters [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=parameter_self_correct] +include::example$snippets/4i_lambda_captures.cpp[tag=parameter_self_correct] ---- == When Are Captures Safe? @@ -63,7 +63,7 @@ Captures are only safe when the lambda object **outlives the coroutine**: [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=stored_lambda_safe,indent=0] +include::example$snippets/4i_lambda_captures.cpp[tag=stored_lambda_safe,indent=0] ---- This pattern is rare. Most async code immediately invokes the lambda and discards it, making captures unsafe. @@ -82,7 +82,7 @@ If the IIFE syntax feels awkward, use a named function instead: [source,cpp] ---- -include::example$snippets/4h_lambda_captures.cpp[tag=named_member_coroutine] +include::example$snippets/4i_lambda_captures.cpp[tag=named_member_coroutine] ---- Member function coroutines work correctly because `this` is an implicit parameter, not a capture. The compiler copies it to the coroutine frame. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc index d3d159cdd..2dc1f7e43 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc @@ -221,7 +221,7 @@ here, because the caller owns the stream and awaits the task immediately. A coroutine reads its parameters when its body runs, not when the call is written. A task that is stored and awaited later outlives its call expression, so a reference parameter can dangle by then. That is why the fan-out example on -xref:4.coroutines/4f.composition.adoc[Concurrent Composition] takes its item by +xref:4.coroutines/4g.composition.adoc[Concurrent Composition] takes its item by value: it collects its tasks first, then awaits them together. ==== diff --git a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc index b89661e28..089aebf19 100644 --- a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc +++ b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc @@ -70,7 +70,7 @@ include::example$custom-executor/custom_executor.cpp[tag=drive,indent=0] cpp:run_async[] enqueues the initial coroutine. `loop.run()` drains the queue, resuming coroutines one by one until all work completes. -`run()` uses `capy::safe_resume(h)` instead of `h.resume()`. This saves and restores the thread-local frame allocator around each resumption, preventing coroutines from spoiling each other's allocator. All custom executor event loops must use cpp:safe_resume[] -- see xref:4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation] for details. +`run()` uses `capy::safe_resume(h)` instead of `h.resume()`. This saves and restores the thread-local frame allocator around each resumption, preventing coroutines from spoiling each other's allocator. All custom executor event loops must use cpp:safe_resume[] -- see xref:4.coroutines/4h.allocators.adoc#_tls_preservation[TLS Preservation] for details. == Output diff --git a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc index 689a78bb3..e8760c08d 100644 --- a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc +++ b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc @@ -5,8 +5,8 @@ Awaiting a `std::execution` (P2300) sender from inside a Capy coroutine. == What This Example Shows -* How to `co_await` a foreign awaitable that does not implement the xref:4.coroutines/4d.io-awaitable.adoc[IoAwaitable protocol] -* How a bridge restores the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] by posting the resumption through the caller's executor +* How to `co_await` a foreign awaitable that does not implement the xref:4.coroutines/4e.io-awaitable.adoc[IoAwaitable protocol] +* How a bridge restores the xref:4.coroutines/4d.executors.adoc#the-same-executor-invariant[same-executor invariant] by posting the resumption through the caller's executor * How a sender's value and error completion channels map onto cpp:io_result[] [NOTE] @@ -46,5 +46,5 @@ result: 1764 == See Also -* xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] — the general pattern and why there is no universal bridge +* xref:4.coroutines/4e.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] — the general pattern and why there is no universal bridge * xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] — the same technique for Asio operations diff --git a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc index 3dee054c2..d2ea31a30 100644 --- a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc +++ b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc @@ -5,9 +5,9 @@ Using Boost.Asio async operations directly inside a Capy coroutine through a `us == What This Example Shows -* How a completion token adapts _any_ Asio async operation into an xref:4.coroutines/4d.io-awaitable.adoc[IoAwaitable] +* How a completion token adapts _any_ Asio async operation into an xref:4.coroutines/4e.io-awaitable.adoc[IoAwaitable] * How the token bridges `std::stop_token` to Asio's cancellation slot -* How the bridge preserves the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] +* How the bridge preserves the xref:4.coroutines/4d.executors.adoc#the-same-executor-invariant[same-executor invariant] [NOTE] ==== @@ -48,5 +48,5 @@ example complete! == See Also -* xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] — the general pattern +* xref:4.coroutines/4e.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] — the general pattern * xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] — the same technique for `std::execution` senders diff --git a/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc b/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc index 001a17508..0e966e020 100644 --- a/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc +++ b/doc/modules/ROOT/pages/8.examples/8q.gui-integration.adoc @@ -47,7 +47,7 @@ include::example$gui-integration/gui_integration.cpp[tag=run,indent=0] The loop blocks while the queue is empty. A GUI loop must do this: the background operation is still running, and an empty queue does not mean the program is finished. `quit()` sets the flag that lets the loop return once the queue drains. -Resumption goes through cpp:safe_resume[] rather than `h.resume()`. This saves and restores the thread-local frame allocator around each resumption. See xref:4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation]. +Resumption goes through cpp:safe_resume[] rather than `h.resume()`. This saves and restores the thread-local frame allocator around each resumption. See xref:4.coroutines/4h.allocators.adoc#_tls_preservation[TLS Preservation]. === Wrapping the Primitive as an Executor @@ -107,7 +107,7 @@ Three contracts combine to put that boundary on the GUI thread: * cpp:task[] propagates that pointer into every `co_await` in its body. A task completes by symmetric transfer and never posts, so nothing in the chain substitutes a different executor. * cpp:run[] records the caller's executor and posts the caller back through it when the inner task completes. That post runs on a pool thread and lands in the GUI queue. -This is the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant] seen from a GUI application. Start the task on the GUI executor, and every line of the body runs on the GUI thread, whatever thread the awaited work ran on. +This is the xref:4.coroutines/4d.executors.adoc#the-same-executor-invariant[same-executor invariant] seen from a GUI application. Start the task on the GUI executor, and every line of the body runs on the GUI thread, whatever thread the awaited work ran on. [NOTE] ==== @@ -128,7 +128,7 @@ cpp:run[] covers work you hand to Capy. A toolkit also completes operations of i include::example$gui-integration/gui_integration.cpp[tag=dialog] ---- -Awaiting that needs an cpp:IoAwaitable[]. xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] covers the protocol. One line of it decides thread affinity: +Awaiting that needs an cpp:IoAwaitable[]. xref:4.coroutines/4e.io-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] covers the protocol. One line of it decides thread affinity: [source,cpp] ---- diff --git a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc index 00644ddc5..f5e82fca7 100644 --- a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc +++ b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc @@ -265,7 +265,7 @@ No heap allocation occurs -- the continuation is embedded in the awaitable, whic This pattern is identical across all five Corosio backends: epoll and io_uring (Linux), kqueue (BSD/macOS), IOCP (Windows), and select (portable fallback). The executor concept and cpp:executor_ref[] provide the abstraction that makes this possible. The backend-specific code deals with I/O readiness or completion notification. The executor-specific code deals with coroutine scheduling. -NOTE: For the TLS save/restore protocol required around `.resume()` calls (cpp:safe_resume[]), including which two call sites are deliberately exempt, see xref:4.coroutines/4g.allocators.adoc#_tls_preservation[TLS Preservation]. +NOTE: For the TLS save/restore protocol required around `.resume()` calls (cpp:safe_resume[]), including which two call sites are deliberately exempt, see xref:4.coroutines/4h.allocators.adoc#_tls_preservation[TLS Preservation]. == Why Not `std::execution` (P2300) diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index bc5af2cfb..07e5f59ea 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -49,6 +49,6 @@ include::example$programs/index_page_echo.cpp[tag=full] * xref:quick-start.adoc[Quick Start] — Set up your first Capy project * xref:2.cpp20-coroutines/2a.foundations.adoc[{cpp}20 Coroutines Tutorial] — Learn coroutines from the ground up * xref:3.concurrency/3a.foundations.adoc[Concurrency Tutorial] — Understand threads, mutexes, and synchronization -* xref:4.coroutines/4a.tasks.adoc[Coroutines in Capy] — Deep dive into cpp:task[task] and the IoAwaitable protocol +* xref:4.coroutines/4b.tasks.adoc[Coroutines in Capy] — Deep dive into cpp:task[task] and the IoAwaitable protocol * xref:5.buffers/5a.buffers.adoc[Buffer Sequences] — Buffer types, sequences, system I/O, and the algorithms over them * xref:6.streams/6a.overview.adoc[Stream Concepts] — Understand the three stream concepts diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index fe81a49f9..591473727 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -104,6 +104,6 @@ include::example$snippets/quick_start.cpp[tag=errors,indent=0] == Next Steps -* xref:4.coroutines/4a.tasks.adoc[Tasks] — Learn how lazy tasks work -* xref:4.coroutines/4b.launching.adoc[Starting Tasks] — Understand cpp:run_async[] in detail -* xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute +* xref:4.coroutines/4b.tasks.adoc[Tasks] — Learn how lazy tasks work +* xref:4.coroutines/4c.launching.adoc[Starting Tasks] — Understand cpp:run_async[] in detail +* xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute diff --git a/doc/modules/ROOT/pages/why-capy.adoc b/doc/modules/ROOT/pages/why-capy.adoc index 94f07c604..e7e6a38b3 100644 --- a/doc/modules/ROOT/pages/why-capy.adoc +++ b/doc/modules/ROOT/pages/why-capy.adoc @@ -12,7 +12,7 @@ include::example$snippets/why_capy.cpp[tag=invariant] [NOTE] ==== -See xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[the same-executor invariant] for the rationale and xref:4.coroutines/4d.io-awaitable.adoc#bridging-a-foreign-awaitable[bridging a foreign awaitable] for the escape hatch. +See xref:4.coroutines/4d.executors.adoc#the-same-executor-invariant[the same-executor invariant] for the rationale and xref:4.coroutines/4e.io-awaitable.adoc#bridging-a-foreign-awaitable[bridging a foreign awaitable] for the escape hatch. ==== == What Capy Is Not diff --git a/test/doc/programs/4b_launching_run_async.cpp b/test/doc/programs/4c_launching_run_async.cpp similarity index 100% rename from test/doc/programs/4b_launching_run_async.cpp rename to test/doc/programs/4c_launching_run_async.cpp diff --git a/test/doc/programs/4c_executors_executor_ref.cpp b/test/doc/programs/4d_executors_executor_ref.cpp similarity index 100% rename from test/doc/programs/4c_executors_executor_ref.cpp rename to test/doc/programs/4d_executors_executor_ref.cpp diff --git a/test/doc/programs/4c_executors_thread_pool.cpp b/test/doc/programs/4d_executors_thread_pool.cpp similarity index 100% rename from test/doc/programs/4c_executors_thread_pool.cpp rename to test/doc/programs/4d_executors_thread_pool.cpp diff --git a/test/doc/snippets/4a_tasks.cpp b/test/doc/snippets/4b_tasks.cpp similarity index 100% rename from test/doc/snippets/4a_tasks.cpp rename to test/doc/snippets/4b_tasks.cpp diff --git a/test/doc/snippets/4b_launching.cpp b/test/doc/snippets/4c_launching.cpp similarity index 100% rename from test/doc/snippets/4b_launching.cpp rename to test/doc/snippets/4c_launching.cpp diff --git a/test/doc/snippets/4c_executors.cpp b/test/doc/snippets/4d_executors.cpp similarity index 100% rename from test/doc/snippets/4c_executors.cpp rename to test/doc/snippets/4d_executors.cpp diff --git a/test/doc/snippets/4d_io_awaitable.cpp b/test/doc/snippets/4e_io_awaitable.cpp similarity index 100% rename from test/doc/snippets/4d_io_awaitable.cpp rename to test/doc/snippets/4e_io_awaitable.cpp diff --git a/test/doc/snippets/4e_cancellation.cpp b/test/doc/snippets/4f_cancellation.cpp similarity index 100% rename from test/doc/snippets/4e_cancellation.cpp rename to test/doc/snippets/4f_cancellation.cpp diff --git a/test/doc/snippets/4f_composition.cpp b/test/doc/snippets/4g_composition.cpp similarity index 100% rename from test/doc/snippets/4f_composition.cpp rename to test/doc/snippets/4g_composition.cpp diff --git a/test/doc/snippets/4g_allocators.cpp b/test/doc/snippets/4h_allocators.cpp similarity index 100% rename from test/doc/snippets/4g_allocators.cpp rename to test/doc/snippets/4h_allocators.cpp diff --git a/test/doc/snippets/4h_lambda_captures.cpp b/test/doc/snippets/4i_lambda_captures.cpp similarity index 100% rename from test/doc/snippets/4h_lambda_captures.cpp rename to test/doc/snippets/4i_lambda_captures.cpp