From e1673d14774e38e6586ca848126105ef595d31cf Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 02:56:16 +0100 Subject: [PATCH 01/12] docs: add synchronization primitives page Add 4l, one conceptual home for async_mutex, async_event and async_waker: choosing between them and a strand, the shared threading model, the cancellation and destruction rules, and allocation. The page shows existing tagged regions only. Link it from the glossary entries, the 4j destruction paragraph (split into shorter sentences), and the Next Steps of 8b, 8f and 8l. --- .../ROOT/pages/4.coroutines/4j.lifetimes.adoc | 8 +- .../4.coroutines/4l.synchronization.adoc | 202 ++++++++++++++++++ .../8.examples/8b.producer-consumer.adoc | 1 + .../8.examples/8f.timeout-cancellation.adoc | 1 + .../ROOT/pages/8.examples/8l.async-mutex.adoc | 1 + doc/modules/ROOT/pages/glossary.adoc | 6 +- 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc diff --git a/doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc b/doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc index 249aebea4..be9eefc45 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc @@ -179,9 +179,11 @@ on the executor of the coroutine that started it. When that executor belongs to the context being destroyed, the unwind runs during the destruction. When it belongs to another context, the unwind runs there later. -Destroying an cpp:async_mutex[], cpp:async_event[] or cpp:async_waker[] is -different: each waiter is resumed with an error, as a stop request would -resume it, and returns through its own code. +Destroying an cpp:async_mutex[], cpp:async_event[], or cpp:async_waker[] is +different. Each waiter resumes with an error, as a stop request would resume +it, and returns through its own code. +xref:4.coroutines/4l.synchronization.adoc#destruction[Synchronization Primitives] +gives the exact rules. That gives the ordering rules: diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc new file mode 100644 index 000000000..dd4dab386 --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -0,0 +1,202 @@ +// +// Copyright (c) 2026 TheLingeringWill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Synchronization Primitives +:page-mode: explanation + +Tasks that share state, wait for each other, or wait for an event outside Capy need a way to wait. +That wait must suspend the task, not block its thread. +Capy provides one serializing executor and three primitives for this. + +== Choosing a Primitive + +[cols="2,1,2,1"] +|=== +| Need | Use | Waiters | Shown in + +| One task at a time touches the shared state +| cpp:strand[] +| Not applicable: the strand runs everything posted to it, one at a time +| xref:4.coroutines/4d.executors.adoc#_strand_serialization_without_mutexes[Executors], xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] + +| Exclusive access to the state, held across a `co_await` +| cpp:async_mutex[] +| Many, resumed one at a time in first-in, first-out order +| xref:8.examples/8l.async-mutex.adoc[Async Mutex] + +| Resume every waiting task when a condition becomes true +| cpp:async_event[] +| Many, all resumed by `set()`; the event stays set until `clear()` +| xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] + +| Resume one task from another thread or library +| cpp:async_waker[] +| One at a time, plus one wakeup kept for the next wait +| xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation], xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] +|=== + +A strand serializes everything that runs on it. +A mutex serializes only its critical section, so other work on the same executor keeps running. +xref:8.examples/8l.async-mutex.adoc#_strand_vs_async_mutex[Strand vs Async Mutex] compares the two. + +== Holding a Lock Across a `co_await` + +A task that finds the cpp:async_mutex[] held suspends instead of blocking its thread. +The waiting tasks form a queue, and `unlock()` posts the first one in it. + +[source,cpp] +---- +include::example$async-mutex/async_mutex.cpp[tag=mutex,indent=0] +---- + +[source,cpp] +---- +include::example$async-mutex/async_mutex.cpp[tag=scoped_lock,indent=0] +---- + +`scoped_lock()` completes with an error code and a guard. +The error code is set when a stop request arrives while the task waits in the queue. +The guard releases the mutex when it leaves its scope. + +In this example, the critical section does not suspend. +A real task often must, for example to write the shared state to a stream. +The guard stays held across any `co_await` in its scope, and the other tasks stay queued until it is released. + +A `std::mutex` cannot do this. +It blocks its thread while it waits. +It must also be unlocked on the thread that locked it, but a task on a thread pool can resume on another thread. + +== Resuming Many Tasks at Once + +An cpp:async_event[] starts unset. +Every task that awaits `wait()` on an unset event suspends. + +[source,cpp] +---- +include::example$producer-consumer/producer_consumer.cpp[tag=event,indent=0] +---- + +The producer calls `set()`, which posts every waiting task to its executor: + +[source,cpp] +---- +include::example$producer-consumer/producer_consumer.cpp[tag=producer,indent=0] +---- + +The consumer waits for that call: + +[source,cpp] +---- +include::example$producer-consumer/producer_consumer.cpp[tag=consumer,indent=0] +---- + +The event stays set until `clear()` is called. +A `wait()` on a set event completes at once, so the order of `set()` and `wait()` does not matter. + +== Resuming a Task from Another Thread + +An cpp:async_waker[] connects a task to an event that Capy does not see. +That event can be a timer thread, a signal, or a callback from another library. +One task suspends in `wait()`, and any thread calls cpp:async_waker::wake[wake]`()`. + +[source,cpp] +---- +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=fetch_channel] +---- + +[source,cpp] +---- +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race_await_fetch] +---- + +A worker thread stores the result in the channel, then calls `wake()` on `fetch_ready`. +xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] shows the whole program. + +`wake()` never resumes the task inline. +It posts the task to the executor that the task awaited from. +A wakeup that arrives before the task waits is kept, and the next `wait()` consumes it at once. +Several wakeups before a wait count as one. +Only one task may wait on a waker at a time. + +[#threading-model] +== The Threading Model + +The three primitives share one threading model. +Every task that awaits a primitive must run on an executor that never runs its continuations concurrently. +Use a single-threaded executor, or a strand over a thread pool. +xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] and xref:8.examples/8l.async-mutex.adoc[Async Mutex] use a strand for this reason. +Awaiting a primitive directly on a multi-threaded executor is undefined. + +The exception is `wake()`, which any thread may call. + +A primitive resumes each waiter by posting it to its executor. +A stop callback can run on any thread, so it also only posts. +xref:4.coroutines/4e.context-awaitable.adoc#stop-callbacks-must-post-not-resume[Stop Callbacks Must Post, Not Resume] explains why. + +== Cancellation + +A waiter observes the stop token of the task that awaits it. +When a stop request arrives while the task is suspended, the task resumes with cpp:error::canceled[error::canceled]. +Compare the result with cpp:cond::canceled[cond::canceled], as xref:4.coroutines/4f.cancellation.adoc#_the_canceled_convention[The canceled Convention] describes. + +Two rules decide what an operation does when a stop is already requested: + +* An operation that can complete at once does so, and ignores the stop token. + This covers a free mutex, a set event, and a waker that holds a wakeup. +* An operation that would suspend completes at once with cpp:error::canceled[error::canceled]. + It does not join the queue. + +A wakeup that loses the race against a stop request is kept for the next `wait()`. + +[#destruction] +== Destruction + +The usual order is to request a stop, wait for the work to end, and then destroy the primitive: + +[source,cpp] +---- +include::example$snippets/4j_lifetimes.cpp[tag=shutdown_order,indent=0] +---- +<1> The waiting task observes the stop token and finishes through its own code. +<2> `join` waits for the outstanding work, then for the worker threads. +<3> The pool, and then `never_set`, are destroyed with no waiter left. + +Destroying a primitive that still has waiters cancels them. +Each waiter is posted to its executor to resume with cpp:error::canceled[error::canceled]. +An executor that can no longer run the waiter abandons it instead, possibly before the destructor returns. +A task that the mutex was handed to, but that has not resumed yet, resumes canceled. +It holds nothing. + +Two preconditions apply: + +* No task holds the cpp:async_mutex[] when it is destroyed. +* No call to `wake()` is in progress on the cpp:async_waker[], or made afterward. + +The reference page of each destructor gives the exact wording. +xref:4.coroutines/4j.lifetimes.adoc#_shutdown_ordering[Shutdown Ordering] explains the order in the general case. + +== Allocation and Lifetime + +No operation on these primitives allocates memory. +Each waiter lives in the frame of the task that awaits. + +The primitives can be neither copied nor moved, because suspended waiters point into them. +Keep each one at a stable address that outlives its waiters. +For example, xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] keeps its wakers in a `std::array`. + +== See Also + +* xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] -- strands and the executors they wrap +* xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] -- how a stop request reaches a waiting task +* xref:4.coroutines/4j.lifetimes.adoc[Lifetime and Ownership] -- the order to shut things down in +* xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] -- a complete program with an event +* xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] -- a waker fed by a timer thread +* xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] -- a strand instead of a mutex +* xref:8.examples/8l.async-mutex.adoc[Async Mutex] -- a complete program with a mutex +* xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] -- wakers fed by a ticker thread diff --git a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc index 844265c1e..d46a709a5 100644 --- a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc +++ b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc @@ -102,3 +102,4 @@ Consumer: received value 42 == Next Steps * xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] — Zero-allocation buffer composition +* xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] — How the event compares with the other primitives diff --git a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc index 869e0323c..abeba732d 100644 --- a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc +++ b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc @@ -146,3 +146,4 @@ Cancelled (returned nullopt) == Next Steps * xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] — Concurrent operations with when_all +* xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] — The rules that the waker shares with the mutex and the event diff --git a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc index 072149b78..4df004a96 100644 --- a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc +++ b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc @@ -88,3 +88,4 @@ Acquisition order: W0 -> W1 -> W2 -> W3 -> W4 -> W5 == Next Steps * xref:8.examples/8m.parallel-tasks.adoc[Parallel Tasks] -- Distributing work across a thread pool +* xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] -- Choosing between a strand, a mutex, an event, and a waker diff --git a/doc/modules/ROOT/pages/glossary.adoc b/doc/modules/ROOT/pages/glossary.adoc index ea029f9f1..61c7e434f 100644 --- a/doc/modules/ROOT/pages/glossary.adoc +++ b/doc/modules/ROOT/pages/glossary.adoc @@ -119,7 +119,7 @@ See cpp:cond[]. [[event]]Event:: A primitive that suspends any number of coroutines until it is set, then -resumes all of them. See cpp:async_event[]. +resumes all of them. See cpp:async_event[] and xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives]. [[execution-context]]Execution Context:: An object that owns the resources work runs on, such as threads, and the @@ -180,7 +180,7 @@ move-only. [[mutex]]Mutex:: A primitive that lets one coroutine at a time hold a lock, suspending the -others instead of blocking their threads. See cpp:async_mutex[]. +others instead of blocking their threads. See cpp:async_mutex[] and xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives]. == O @@ -312,7 +312,7 @@ A single-slot primitive that suspends one coroutine until a wakeup arrives from any thread. A wakeup that arrives before the coroutine waits is kept for its next wait. Used to connect external events, such as timers, to coroutines. -See cpp:async_waker[]. +See cpp:async_waker[] and xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives]. [[when-all]]`when_all`:: Awaiting several tasks concurrently and completing when all of them have From cfc5b1cae678e3d70275458c98d1dde57dd18ed1 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 02:56:16 +0100 Subject: [PATCH 02/12] docs: add early quitter example page Add 8r, which documents example/early-quitter as a user-written task type: when to use it instead of task, the signal-to-stop pattern, how a stop ends a run of quitters, results that race a stop, the boundary rule, cleanup after the completion handler, the self-checks and the known limitations. It keeps the still-valid rationale of the deleted doc/quitter.md. Link it from 4e and from a TIP in 4f, and say in 4f that when_any cancels the losers when the first task succeeds. --- .../4.coroutines/4e.context-awaitable.adoc | 1 + .../pages/4.coroutines/4f.cancellation.adoc | 9 +- .../pages/8.examples/8r.early-quitter.adoc | 410 ++++++++++++++++++ 3 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc diff --git a/doc/modules/ROOT/pages/4.coroutines/4e.context-awaitable.adoc b/doc/modules/ROOT/pages/4.coroutines/4e.context-awaitable.adoc index cf2d13ec6..b6487937a 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4e.context-awaitable.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4e.context-awaitable.adoc @@ -85,6 +85,7 @@ These methods exist because launcher functions like cpp:run_async[] cannot `co_a Launcher functions access `set_continuation` and `set_environment` through the typed handle provided by `handle()`. Capy's cpp:task[task] satisfies this concept. +The xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] example builds a second task type that satisfies it, outside the library. == How Context Flows diff --git a/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc b/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc index b19b53a12..342ac26ee 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc @@ -368,9 +368,16 @@ Cancel all pending work during shutdown: include::example$snippets/4f_cancellation.cpp[tag=graceful_shutdown,indent=0] ---- +[TIP] +==== +For a task whose body ends at its current `co_await` with no checks, see xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)]. +That task type is example code, not part of Capy. +==== + === 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/4g.composition.adoc[Concurrent Composition]. +cpp:when_any[] uses stop tokens internally to cancel "losing" tasks when the first task succeeds. This is covered in xref:4.coroutines/4g.composition.adoc[Concurrent Composition]. +xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] shows a complete race. == The Standard Library Types diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc new file mode 100644 index 000000000..4892b4fc1 --- /dev/null +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -0,0 +1,410 @@ +// +// Copyright (c) 2026 TheLingeringWill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Writing Your Own Task Type (Early Quitter) +:page-mode: how-to + +include::partial$diagram-style.adoc[] + +Build a task type whose coroutines end at their current `co_await` when a stop is requested. +The bodies of these coroutines contain no cancellation checks. +The type uses only the public cpp:ContextAwaitable[] protocol from xref:4.coroutines/4e.context-awaitable.adoc[The ContextAwaitable Protocol]. + +[NOTE] +.Example code, not library API +==== +`early::quitter` lives in `example/early-quitter/quitter.hpp`. +It is not part of Capy: it has no reference page and no stability guarantee. +Copy it and adapt it. +Capy contained a `quitter` type until https://github.com/cppalliance/capy/pull/481[pull request #481] moved it into this example. +https://github.com/cppalliance/capy/discussions/431[Discussion #431] records that decision. +==== + +== What This Example Shows + +* A second task type that satisfies cpp:ContextAwaitable[] and cpp:ContextRunnable[], as xref:4.coroutines/4e.context-awaitable.adoc#_contextrunnable_concept[ContextRunnable Concept] defines it +* A stop policy that ends the body instead of resuming it +* Shutting down on `Ctrl+C`, with RAII cleanup across a chain of quitters +* The constraints that a task type built outside the library accepts + +[NOTE] +.Why this example exists +==== +An application that shuts down wants every task in flight to end cleanly. +With cpp:task[], each `co_await` that can see the stop needs a check such as `if(ec) co_return;` after it. +The sender model avoids those checks with `set_stopped`, because the stopped coroutine never resumes. +A quitter gives a Capy coroutine the same behavior. +==== + +== Choosing Between `task` and a Quitter + +Capy's only task type is cpp:task[task], and Capy has no separate `io_task`. +Use cpp:task[] by default. + +A quitter fits when a stop always means "drop the work and release its resources." +Application shutdown is the usual case, especially across call chains with many suspension points. +The header comment of `shutdown.cpp` shows the alternative: with cpp:task[], each wait needs `if(ec) co_return;` after it. + +[cols="1,2,2"] +|=== +| | `task` | `early::quitter` + +| Status +| A library type with a reference page +| Example code in namespace `early` + +| On a stop +| The body resumes, and stop-aware operations report `canceled` +| The body is not resumed: when the pending operation completes, the run of quitters ends + +| Checks in the body +| Wherever the body must react to a stop +| None + +| What the first awaiter outside the quitters receives +| The result of the task +| `canceled`, as the result of the quitter + +| Result type at that boundary +| Any `T` +| `results<...>` or `std::error_code`, checked at compile time + +| When the locals of a stopped coroutine are destroyed +| Before the completion handler runs +| After the completion handler returns, under cpp:run_async[] + +| A stop requested before the start +| The body runs and sees the stop through its checks and its operations +| The body runs to its first `co_await`, which stops without starting that operation + +| Cost of each `co_await` +| No stop check of its own +| Two stop checks; a `co_await` that suspends also resumes a gate frame +|=== + +xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] describes how a cpp:task[] observes a stop. + +[NOTE] +.Why a separate type +==== +The task type is a policy point. +A program that wants explicit error handling uses cpp:task[]. +A program that wants a stop to end the work without checks uses a quitter. +The cpp:ContextAwaitable[] protocol does not require one task type, so both work with the same launchers and combinators. +xref:9.design/9m.WhyNotAsio.adoc#_coroutine_execution_model[Coroutine Execution Model] lists user-defined task types among the features of the protocol. +==== + +== Source Code + +[source,cpp] +---- +include::example$early-quitter/main.cpp[] +---- + +.`quitter.hpp` +[%collapsible] +==== +[source,cpp] +---- +include::example$early-quitter/quitter.hpp[] +---- +==== + +.`shutdown.cpp` +[%collapsible] +==== +[source,cpp] +---- +include::example$early-quitter/shutdown.cpp[] +---- +==== + +The self-checks are in `checks.cpp`, in the https://github.com/cppalliance/capy/tree/develop/example/early-quitter[example directory]. + +== Build + +[source,cmake,role=external] +---- +add_executable(capy_example_early_quitter main.cpp quitter.hpp) +target_link_libraries(capy_example_early_quitter Boost::capy) + +add_executable(capy_example_early_quitter_shutdown shutdown.cpp quitter.hpp) +target_link_libraries(capy_example_early_quitter_shutdown Boost::capy) + +add_executable(capy_example_early_quitter_checks checks.cpp quitter.hpp) +target_link_libraries(capy_example_early_quitter_checks Boost::capy) +add_test(NAME capy_example_early_quitter_checks + COMMAND capy_example_early_quitter_checks) +---- + +The directory has a `CMakeLists.txt` but no `Jamfile`, so `b2` does not build it. +CI builds the three programs and runs the checks. + +== Walkthrough + +=== A Call Chain with No Cancellation Checks + +In `main.cpp`, `fetch` and `store` each await the waker twice. +Each wait stands in for one I/O operation. +`process_item` awaits `fetch`, then `store`, and `worker` calls `process_item` in an endless loop. + +None of these functions looks at a stop token. +Each `resource_guard` stands in for a connection, a file, or a buffer, and logs its release. +With cpp:task[], every wait would need the check from xref:8.examples/8f.timeout-cancellation.adoc#_checking_for_cancellation[Checking for Cancellation]. +xref:4.coroutines/4f.cancellation.adoc#_checking_the_token[Checking the Token] describes that check. + +Only `worker` returns `early::quitter`, because it is the top of the run, and its result must carry the stop. +The other three can return any result type, because only another quitter awaits them. + +=== Starting the Workers + +The cpp:run_async[] loop at the end of `main` starts one `worker` on each strand. +The completion handler takes the result of the worker, a `std::error_code`. +After a stop, that result is `canceled`. + +Each worker gets its own strand and its own cpp:async_waker[]. +A waker needs an executor that never runs its continuations concurrently, as xref:4.coroutines/4l.synchronization.adoc#threading-model[The Threading Model] explains. + +=== Shutting Down from a Signal + +The signal handler `on_signal` only sets `g_signaled`, a lock-free atomic flag. +It cannot call `request_stop()` on `g_stop`, because that call runs the stop callbacks, which post and allocate. +Neither is safe inside a signal handler. + +The ticker thread in `main` checks the flag every `20 ms`. +Once the flag is set, the ticker calls `g_stop.request_stop()`. +On each tick, the ticker also wakes every waker, standing in for I/O completions. + +Capy has no signal facility. +Signal handling needs a platform reactor, so for now it belongs in Corosio, as xref:9.design/9b.Separation.adoc#_what_lives_where[What Lives Where] explains. +https://github.com/cppalliance/capy/discussions/432[Discussion #432] about signals in Capy is still open. + +=== Operations Resume a Gate + +The promise of a quitter wraps each awaited operation in a `transform_awaiter`, in `quitter.hpp`. +Its `await_suspend` hands the operation the handle of a gate instead of the handle of the body. +A child quitter receives the same gate handle. + +The gate is a small coroutine frame of type `gate`. +`make_gate` creates it the first time a quitter suspends, and `~promise_base` destroys it. +The gate loops on `decide_awaiter`, which calls `promise_base::decide` each time an operation resumes the gate. +`decide` resumes the body, unless a stop is pending and the result ends the quitter. + +The quitter never abandons an operation in flight. +It acts when that operation completes. +A stop-aware operation, such as the `wait()` of an cpp:async_waker[], completes at once with `canceled`, because its stop callback posts the resumption. + +=== A Stop Ends the Run from the Top + +When `decide` ends the quitter, it calls `promise_base::stop`. +That function walks the `parent_` pointers to the topmost quitter of the run, and marks each quitter on the way as stopped. +It stores `canceled` as the result of the top quitter, then returns the continuation of that quitter. +No body in the run resumes. + +[mermaid] +---- +sequenceDiagram + autonumber + accTitle: A stop request ends a run of quitters + accDescr: The ticker thread requests a stop. The stop callback of the waker posts the gate of the waiting quitter, fetch or store, with a canceled result. The gate decides that the stop ends the quitter. The stop function walks to the topmost quitter, worker, and stores canceled as its result. run_async resumes, and its completion handler receives canceled. run_async then destroys the frame of worker, which destroys the frames below it, so the RAII guards run at every level. + participant T as Ticker thread + participant W as async_waker + participant G as Gate of fetch or store + participant K as worker + participant R as run_async + T->>W: g_stop.request_stop() runs the stop callback + W->>G: post the gate, the result is canceled + G->>G: decide(): a stop is pending, and the result is canceled + G->>K: stop() walks to the top quitter and stores canceled + K->>R: resume the continuation of the top quitter + R->>R: the completion handler receives canceled + R->>K: destroy the top frame, then the frames below it +---- + +[NOTE] +.Why not an exception +==== +The quitter that Capy used to contain ended a stopped body by throwing an internal exception. +In C++, a throw is the only way to reach `final_suspend` from a suspension point. +This example never resumes the body, so it needs no exception and no support from the library. +==== + +=== Results That Race a Stop + +`transform_awaiter::await_ready` and `transform_awaiter::await_suspend` decide what a stop does to an operation. +There are two cases. + +*A stop is already pending when the `co_await` begins.* +`await_ready` checks for the stop before it asks the operation anything, and reports that the operation is not ready. +`await_suspend` then calls `stop()`. +The operation is never started, so nothing, such as a lock or a buffer, is acquired only to be dropped. +Nothing is delivered, and the run stops. + +*A stop arrives while the operation completes.* +The stop can arrive inside the readiness check of the operation. +It can also arrive after the operation resumes the gate, but before the gate decides. +In both cases, `stops_on_completion` takes the result early: + +* A `canceled` result ends the quitter. +* Any other `results<...>` or `std::error_code` value is delivered to the body. + The stop takes effect at the next `co_await`. +* A result that cannot carry the stop, such as `void`, a reference, or any other type, is dropped, and the quitter ends. + The operation still hands the result over, so anything it owns is destroyed. +* An exception from the operation reaches the body. + The stop takes effect at the next `co_await`. + +The constant `holds` in `transform_awaiter` decides which results the quitter can keep. +It uses the trait `carries_stop`, described in the next section. + +=== The Boundary Rule + +A stop becomes a value at the first awaiter that is not a quitter. +That awaiter can be a task, cpp:run[], cpp:run_async[], or cpp:when_all[], and it receives `canceled` as the result of the quitter. +So the result type at that boundary must be able to hold `canceled`. + +The trait `carries_stop` accepts `std::error_code` and any `std::tuple`, which includes cpp:results[]. +The `static_assert` in `set_continuation` checks a quitter that a launcher starts. +A second `static_assert`, in `quitter::await_suspend`, checks a quitter that a task or a combinator awaits. +A quitter with any other result type at the boundary is a compile error. + +=== Cleanup Runs After the Completion Handler + +A stopped quitter keeps its locals until its owner destroys the frame. +Under cpp:run_async[], the completion handler runs first, and then the frame is destroyed. +So a completion handler that counts down a latch can wake `main` before the resource guards run. + +`shutdown.cpp` handles this in `main`. +After `done.wait()`, it joins the ticker, then calls `pool.join()`, and only then reads the counters that the guards update. + +`main.cpp` prints its final message without calling `pool.join()`. +Pool threads can still be destroying frames, so `released` lines can follow that message. +Only the order of the output depends on this, because `main.cpp` reads no state that the guards update. +Destroying the strands and the wakers before the pool is safe here, and the destructor of the pool joins its threads. + +[WARNING] +==== +* Wait for the work of the executor, for example with `thread_pool::join`, before you read state that the destructors of a quitter touch. +* A quitter that another coroutine or a combinator awaits may be destroyed unfinished in two cases only. + Either its run stopped, or an abandoned chain unwinds it. + In any other case, `destroy_frame` calls `std::terminate`. +==== + +== What the Self-Checks Cover + +`checks.cpp` runs 13 checks on a single-threaded queue, except the last one, which uses a thread pool: + +* `value_without_stop`: without a stop, the value of a quitter reaches the completion handler unchanged. +* `deep_stop_while_suspended`: a stop ends three nested quitters at once, no body resumes, and all three guards run. +* `stop_before_start`: with the stop requested before the start, the body runs up to its first `co_await` and ends there. +* `ready_success_then_stop_delivers`: a stop arrives while a ready operation completes, the successful result is delivered, and the next `co_await` stops. +* `ready_canceled_then_stop_stops`: the same race with a `canceled` result ends the quitter at once. +* `stop_pending_leaves_operation_untouched`: with a stop pending at a `co_await`, the operation is never started. +* `early_exception_reaches_body`: an exception from a result taken early reaches the body. +* `stop_after_wake_before_gate`: a stop arrives after a wake posts the gate, but before the gate runs. + The wake is delivered, and the next `co_await` stops. +* `task_between_quitters`: a task between two quitters receives `canceled` as a value, and the outer quitter then ends. +* `exception_propagates`: an exception from the body of a child quitter reaches the parent quitter. +* `teardown_through_closed_executor`: the waker posts the gate to a closed executor, which abandons it. + The chain unwinds, and no completion handler runs. +* `when_all_branches_stop`: two quitters in the branches of cpp:when_all[] both stop, and the task that awaits cpp:when_all[] resumes. +* `stop_races_wake`: a wake and a stop from two threads race 2000 times against a quitter on a thread pool. + Each run completes exactly once, and its frame always unwinds. + +Run them with `ctest --test-dir build -R early_quitter`. + +== Known Limitations + +* A stop requested before the start still lets the body run to its first `co_await`. +* A result that cannot carry the stop is dropped when a stop arrives as the operation completes. + That includes a reference result, and a `const` move-only result is moved. +* A stopped `results` needs default-constructible `Ts`. +* cpp:task[] works around a symmetric-transfer limit of MSVC, and the quitter does not. +* The checks do not cover cpp:run[] onto another executor, cpp:when_any[], or a handoff through a strand or a mutex. + +== Output + +`capy_example_early_quitter`, with `Ctrl+C` pressed after about one second. +The interleaving of the lines varies from run to run. + +[role=output] +---- +Press Ctrl+C to quit. + released connection for item 0 + released connection for item 0 + released connection for item 0 + released connection for item 0 + stored WORKER 0 ITEM 0 + stored WORKER 1 ITEM 0 + released output file + released output file + stored WORKER 2 ITEM 0 + released output file + stored WORKER 3 ITEM 0 + released output file +... + released connection for item 8 + released connection for item 8 + released connection for item 8 + released connection for item 8 + released output file + released output file + released worker 2 + released worker 1 + released output file + released worker 3 + released output file + released worker 0 +Shutdown complete. +---- + +The last lines show the stop at work. +Each worker had fetched item 8 and was waiting inside `store`. +The stop releases its output file and its worker guard, and nothing more is stored. +In this run, every `released` line came before `Shutdown complete.`, but another run can print some of them after it. +For code that reads state after shutdown, copy the order in `shutdown.cpp`: `ticker.join()`, then `pool.join()`, then read the state. + +`capy_example_early_quitter_shutdown` prints a summary after the cleanup: + +[role=output] +---- +Starting 4 workers. Press Ctrl+C to quit. + + [worker 0] acquired resources + [worker 2] acquired resources + [worker 1] acquired resources + [worker 3] acquired resources + [worker 0] processing item 0 + [worker 1] processing item 0 +... + [worker 1] processing item 7 + [worker 0] released resources (cleanup) + [worker 3] released resources (cleanup) + [worker 2] released resources (cleanup) + [worker 1] released resources (cleanup) + +Shutdown complete. + Items processed: 32 + Workers cleaned up: 4/4 +---- + +== Exercises + +. Rewrite `worker` and the functions it calls as cpp:task[] coroutines, with the checks they then need. +. Await `worker` from a cpp:task[] and branch on cpp:cond::canceled[cond::canceled]. +. Add `pool.join()` to `main.cpp` before the final message, and compare where the `released` lines appear. +. Build `checks.cpp` with `-fsanitize=thread` and run it. + +== See Also + +* xref:4.coroutines/4e.context-awaitable.adoc[The ContextAwaitable Protocol] -- the protocol a task type implements +* xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] -- how a cpp:task[] observes a stop +* xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] -- the waker that the workers wait on +* xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] -- stop handling with cpp:task[] +* xref:9.design/9b.Separation.adoc[Why Capy Is Separate] -- why signals belong in Corosio +* xref:9.design/9m.WhyNotAsio.adoc#_coroutine_execution_model[Coroutine Execution Model] -- user-defined task types in the comparison with Boost.Asio From 9ecc82a8a09108e44ff1510487fa3885d2d02b1e Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 02:56:16 +0100 Subject: [PATCH 03/12] docs: add asio callbacks example page Add 8t, which walks through asio_callbacks.cpp and the uni_stream adapter built on async_initiate. Point 8p's note and See Also at 8t and at the 9n section that covers any_stream.cpp. --- .../pages/8.examples/8p.asio-use-capy.adoc | 3 + .../pages/8.examples/8t.asio-callbacks.adoc | 127 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc 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 d979997b2..2bd176523 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 @@ -12,6 +12,8 @@ Using Boost.Asio async operations directly inside a Capy coroutine through a `us [NOTE] ==== This example is built when a `Boost::asio` target is available. The full source is in `example/asio/` (`use_capy_example.cpp` and the reusable token in `api/use_capy.hpp`). +The same directory holds two more programs. +xref:8.examples/8t.asio-callbacks.adoc[Using a Capy Stream from Asio] describes `asio_callbacks.cpp`, and xref:9.design/9n.WhyNotCobalt.adoc#_the_dimovian_ideal[The Dimovian Ideal] describes `any_stream.cpp`. ==== == The Completion Token @@ -50,3 +52,4 @@ example complete! * xref:4.coroutines/4e.context-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` xref:glossary.adoc#sender[senders] +* xref:8.examples/8t.asio-callbacks.adoc[Using a Capy Stream from Asio] — the other direction, a Capy stream driven by Asio completion tokens diff --git a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc new file mode 100644 index 000000000..25848e7a5 --- /dev/null +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -0,0 +1,127 @@ +// +// Copyright (c) 2026 TheLingeringWill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Using a Capy Stream from Asio +:page-mode: how-to + +Drive a Capy cpp:any_stream[] with Asio completion tokens, here plain callbacks. + +[NOTE] +==== +This example is built when a `Boost::asio` target is available, and CI does not build it. +The full source is in `example/asio/`: `asio_callbacks.cpp`, and the adapter in `api/uni_stream.hpp`. +Both use `api/capy_streams.hpp`, which defines `asio_context` and `make_stream_pair`. +==== + +== What This Example Shows + +* An adapter, `uni_stream`, that gives a cpp:any_stream[] the Asio universal asynchronous model with `net::async_initiate` +* Each operation starting a Capy task with cpp:run_async[], on an executor that posts through Asio +* `net::async_write` and `net::async_read` composed over the adapter +* Two callback chains that share one Asio `io_context` + +== Source Code + +[source,cpp] +---- +include::example$asio/asio_callbacks.cpp[] +---- + +.`api/uni_stream.hpp` +[%collapsible] +==== +[source,cpp] +---- +include::example$asio/api/uni_stream.hpp[] +---- +==== + +== Build + +[source,cmake,role=external] +---- +add_executable(capy_example_asio_callbacks + asio_callbacks.cpp + api/capy_streams.cpp) +target_link_libraries(capy_example_asio_callbacks + Boost::capy + Boost::asio) +---- + +`b2` builds the same program as `exe asio_callbacks` from `example/asio/Jamfile`. + +== Walkthrough + +=== The Initiation + +`uni_stream` holds a cpp:any_stream[] and a pointer to the `asio_context`. +Its `async_read_some` passes three things to `net::async_initiate`: an `initiate_async_read_some` object, the completion token, and the buffers. +The completion token decides what `async_read_some` returns. +With a plain callback, it returns nothing. + +Asio then calls `initiate_async_read_some` with the completion handler and the buffers. +The initiation looks up the executor associated with the completion handler. +The default is the executor of the `io_context`. + +The initiation wraps that executor in `asio_executor_wrapper`, a Capy executor that posts every continuation through Asio. +Then it starts a task with cpp:run_async[] on the wrapper. +The task converts the buffers with `capy::from_asio`, awaits `read_some` on the cpp:any_stream[], and calls the completion handler with the error code and the byte count. + +The wrapper posts even when Capy asks it to dispatch. +So the completion handler never runs inside the call to `async_read_some`, and it runs on the associated executor, as Asio requires. +`async_write_some` and `initiate_async_write_some` follow the same pattern for writes. + +=== A Callback Chain + +`do_write` keeps its state in a `std::shared_ptr`, so every copy of the function object shares it. +Each call writes one chunk of up to 128 bytes with `net::async_write`. +That composed operation calls `async_write_some` on the adapter until the whole chunk is written. + +The completion handler of each write adds the bytes written, prints the total, and calls a copy of `do_write` again. +The chain ends when it has written 1024 bytes. +`do_read` is the same chain for `net::async_read`. + +`main` makes a connected pair of adapters with `make_uni_pair`, starts both chains, and calls `ctx.run()`. +The call returns when both chains end and no work remains. + +== Output + +[role=output] +---- +writer: wrote 128 bytes (total 128) +reader: read 128 bytes (total 128) +writer: wrote 128 bytes (total 256) +reader: read 128 bytes (total 256) +writer: wrote 128 bytes (total 384) +reader: read 128 bytes (total 384) +writer: wrote 128 bytes (total 512) +reader: read 128 bytes (total 512) +writer: wrote 128 bytes (total 640) +reader: read 128 bytes (total 640) +writer: wrote 128 bytes (total 768) +reader: read 128 bytes (total 768) +writer: wrote 128 bytes (total 896) +reader: read 128 bytes (total 896) +writer: wrote 128 bytes (total 1024) +writer: done, wrote 1024 bytes +reader: read 128 bytes (total 1024) +reader: done, read 1024 bytes +example complete! +---- + +== Exercises + +. Rewrite the reader as an Asio coroutine with `net::co_spawn`, using the `net::use_awaitable` form that the comment on `uni_stream` shows. +. Bind the write handler to an Asio strand with `net::bind_executor`, and confirm that the completion handler runs on that strand. + +== See Also + +* xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] -- the other direction, Asio operations awaited from a Capy coroutine +* xref:9.design/9n.WhyNotCobalt.adoc#_the_dimovian_ideal[The Dimovian Ideal] -- `any_stream.cpp`, stream algorithms written with no Asio headers +* xref:4.coroutines/4e.context-awaitable.adoc#bridging-a-foreign-awaitable[Bridging a Foreign Awaitable] -- the general pattern for connecting another library From 9478ccefc7627706e9d40ae7b3eb53a75ea958b5 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 02:56:17 +0100 Subject: [PATCH 04/12] docs: link the new pages and list the remaining examples Add 4l, 8t and 8r to the nav, the chapter intros and the index. In 8.intro, state that example code is not Capy API, correct the claim that every example is self-contained, and add a table of the programs that have no page, with a nav entry. when-any-cancellation is listed as out of date. Rewrite example/README.md as an index of all 24 example directories. Also add the missing 4j entry to 4.intro. --- doc/modules/ROOT/nav.adoc | 4 + .../ROOT/pages/4.coroutines/4.intro.adoc | 4 + .../ROOT/pages/8.examples/8.intro.adoc | 75 +++++++++++++- doc/modules/ROOT/pages/index.adoc | 2 + example/README.md | 98 +++++++++---------- 5 files changed, 133 insertions(+), 50 deletions(-) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 65a8a530a..48fd73af8 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -9,6 +9,7 @@ ** xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] ** xref:4.coroutines/4e.context-awaitable.adoc[The ContextAwaitable Protocol] ** xref:4.coroutines/4f.cancellation.adoc[Stop Tokens and Cancellation] +** xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] ** 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] @@ -40,7 +41,10 @@ ** xref:8.examples/8n.custom-executor.adoc[Custom Executor] ** xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] ** xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] +** xref:8.examples/8t.asio-callbacks.adoc[Using a Capy Stream from Asio] ** xref:8.examples/8q.gui-integration.adoc[GUI Integration] +** xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] +** xref:8.examples/8.intro.adoc#other-programs-in-the-repository[Other Programs in the Repository] * xref:9.design/9.intro.adoc[Design] ** xref:9.design/9a.CapyLayering.adoc[Layered Abstractions] ** xref:9.design/9b.Separation.adoc[Why Capy Is Separate] diff --git a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc index 646fe7ca5..bf9f3752f 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4.intro.adoc @@ -29,9 +29,13 @@ Capy's coroutine model is built around a single principle: asynchronous code sho xref:glossary.adoc#stop-token[stop token] propagate through a chain of awaited coroutines. * 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/4l.synchronization.adoc[Synchronization Primitives] -- Choosing between a strand, + cpp:async_mutex[], cpp:async_event[], and cpp:async_waker[], and the rules they share. * xref:4.coroutines/4g.composition.adoc[Concurrent Composition] -- Running tasks concurrently with cpp:when_all[] and cpp:when_any[]. * xref:4.coroutines/4h.allocators.adoc[Frame Allocators] -- How xref:glossary.adoc#coroutine-frame[coroutine frames] are allocated, and how to customize the allocator. * xref:4.coroutines/4i.lambda-captures.adoc[Lambda Coroutine Captures] -- A critical pitfall: lambda captures versus coroutine frame lifetime. +* xref:4.coroutines/4j.lifetimes.adoc[Lifetime and Ownership] -- What a coroutine frame keeps + alive, and the order to shut things down in. diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index d3d981ae3..a540483f4 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -13,11 +13,20 @@ These examples demonstrate how the pieces you have learned--tasks, buffers, streams, xref:glossary.adoc#cancellation[cancellation], composition--fit together in practice. -Every example is self-contained and compiles as a standalone program. +Each example builds from its own directory under `example/`. +Some directories hold several programs that share a header. +Each page names any optional dependency that its program needs. For a reading order for your role, start with xref:index.adoc#protocol-anatomy[Protocol Anatomy]. It lists what to read next for application developers, framework authors, and I/O library authors. +== Library Code and Example Code + +The code under `example/`, headers included, is not part of Capy. +It has no reference page and no stability guarantee, even where it declares names in namespace `boost::capy`. +Copy it and adapt it to your program. +For example, the quitter task type on xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] is example code, not a Capy type. + == What This Section Covers * xref:8.examples/8a.hello-task.adoc[Hello Task] -- The minimal Capy program: a task that @@ -46,5 +55,69 @@ It lists what to read next for application developers, framework authors, and I/ `std::execution` (P2300) xref:glossary.adoc#sender[sender] from a Capy coroutine. * xref:8.examples/8p.asio-use-capy.adoc[Calling Asio from a Capy Coroutine] -- Using Boost.Asio operations directly through a `use_capy` completion token. +* xref:8.examples/8t.asio-callbacks.adoc[Using a Capy Stream from Asio] -- Driving a Capy + stream with Asio completion tokens, through an adapter built on `async_initiate`. * xref:8.examples/8q.gui-integration.adoc[GUI Integration] -- Running a Capy coroutine on a GUI event loop, resuming on the GUI thread to update widgets. +* xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] -- A second + task type, whose coroutines a stop ends at their current `co_await` with no checks. + +[#other-programs-in-the-repository] +== Other Programs in the Repository + +These programs have no page of their own. +Some need an optional compiler or library, and some only check that code compiles. + +[cols="2,3,3"] +|=== +| Directory | What it is | How it is built + +| https://github.com/cppalliance/capy/tree/develop/example/asio[`asio/any_stream.cpp`] +| Stream algorithms written against a header with no Asio includes. +xref:9.design/9n.WhyNotCobalt.adoc#_the_dimovian_ideal[The Dimovian Ideal] walks through it. +| The `capy_example_any_stream` target, when a `Boost::asio` target exists, or the `b2` target `any_stream` + +| https://github.com/cppalliance/capy/tree/develop/example/awaitable-sender[`awaitable-sender/`] +| A Capy awaitable wrapped as a `stdexec` sender, the reverse of xref:8.examples/8o.sender-bridge.adoc[Bridging a P2300 Sender] +| `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` and C++23. +The build fetches NVIDIA stdexec. +Its test is `capy_example_awaitable_sender_test`. + +| https://github.com/cppalliance/capy/tree/develop/example/any-sender-size[`any-sender-size/`] +| A measurement of the operation state and the allocations of `exec::any_sender` +| The same flag as `awaitable-sender/`. +Its test is `capy_example_any_sender_size`. + +| https://github.com/cppalliance/capy/tree/develop/example/fabrics[`fabrics/`] +| Compile-only listings from P4251R0 for HPC fabric libraries +| The default build. +Each fabric check builds when the build finds its library. + +| https://github.com/cppalliance/capy/tree/develop/example/cuda/batched-write[`cuda/batched-write/`] +| A batched `write_some` to a CUDA device stream +| `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and the CUDA toolkit. +Its test is `capy_example_cuda_batched_write`. + +| https://github.com/cppalliance/capy/tree/develop/example/cuda/datamovement[`cuda/datamovement/`] +| Compile-only CUDA data-movement listings from P4251R0 +| The CUDA flag + +| https://github.com/cppalliance/capy/tree/develop/example/cuda/notification-strategies[`cuda/notification-strategies/`] +| One GPU completion, awaited in three different ways +| The CUDA flag + +| https://github.com/cppalliance/capy/tree/develop/example/cuda/pipeline[`cuda/pipeline/`] +| Capy tasks and senders on the `stream_scheduler` of NVIDIA `nvexec` +| The CUDA flag, plus `BOOST_CAPY_BUILD_NVEXEC_EXAMPLES=ON`. +That flag needs `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON` and C++23. + +| https://github.com/cppalliance/capy/tree/develop/example/echo-server-corosio[`echo-server-corosio/`] +| An echo server on Corosio +| The default build, when a `Boost::corosio` target exists, or its own `Jamfile` + +| https://github.com/cppalliance/capy/tree/develop/example/when-any-cancellation[`when-any-cancellation/`] +| A race of three data sources with cpp:when_any[]. +*Out of date:* its comments say that the first child to complete wins, and its losing children report success. +For the current rules, see xref:4.coroutines/4f.cancellation.adoc#_the_canceled_convention[The canceled Convention] and xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation]. +| The default build, or `b2` +|=== diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 19f8bd671..37554d037 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -153,6 +153,7 @@ The rest of the diagram is Capy, or an I/O library such as Corosio. * A task resumes on the executor it started with. State that only one task uses needs no lock. State that tasks share on a thread pool needs a strand or an cpp:async_mutex[]. + xref:4.coroutines/4l.synchronization.adoc[Synchronization Primitives] compares the options. * Start top-level work from `main` with cpp:run_async[]. Inside a task, await other tasks directly. To run a child task on a different executor, await it through cpp:run[]. @@ -232,6 +233,7 @@ Read next: * xref:4.coroutines/4h.allocators.adoc[Frame Allocators] * xref:4.coroutines/4j.lifetimes.adoc#_who_owns_a_suspended_coroutine[Who Owns a Suspended Coroutine] * xref:9.design/9l.RunApi.adoc[Run API] +* xref:8.examples/8r.early-quitter.adoc[Writing Your Own Task Type (Early Quitter)] === I/O Library Authors diff --git a/example/README.md b/example/README.md index 0b999bda7..1c14c608b 100644 --- a/example/README.md +++ b/example/README.md @@ -2,55 +2,53 @@ This directory contains example programs demonstrating Boost.Capy usage. -## Examples - -### hello-task/ - -Minimal Capy program: a task that prints a message. - -### producer-consumer/ - -Two tasks communicating via an async event. - -### buffer-composition/ - -Composing buffer sequences without allocation for scatter/gather I/O. - -### mock-stream-testing/ - -Unit testing protocol code with mock streams and error injection. - -### type-erased-echo/ - -Echo server demonstrating the compilation firewall pattern. - -### timeout-cancellation/ - -Using stop tokens to implement operation timeouts. - -### parallel-fetch/ - -Running multiple operations concurrently with `when_all`. - -### echo-server-corosio/ - -A complete echo server using Corosio for real network I/O. Requires Corosio. - -### stream-pipeline/ - -Data transformation through a pipeline of sources and sinks. - -### any-sender-size/ - -Measures the `exec::any_sender` operation state and the heap allocation its -`connect` performs, against the concrete operation state for the same -pipeline. Requires stdexec (`BOOST_CAPY_BUILD_P2300_EXAMPLES=ON`). - -### cuda/batched-write/ - -Runs the batched `write_some` of the CUDA device stream: three buffers, one -await, device holds the concatenation. Requires CUDA -(`BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON`). +The code here, headers included, is example code and not part of Capy. It has +no reference documentation and no stability guarantee, even where it declares +names in namespace `boost::capy`. Copy it and adapt it. + +## Taught in the manual + +Each of these directories has a page in the +[Example Programs](https://develop.capy.cpp.al/capy/8.examples/8.intro.html) +section of the manual. + +| Directory | Page | What it shows | +| --- | --- | --- | +| `hello-task/` | [Hello Task](https://develop.capy.cpp.al/capy/8.examples/8a.hello-task.html) | Minimal Capy program: a task that prints a message | +| `producer-consumer/` | [Producer-Consumer](https://develop.capy.cpp.al/capy/8.examples/8b.producer-consumer.html) | Two tasks communicating via an async event, on a strand | +| `buffer-composition/` | [Buffer Composition](https://develop.capy.cpp.al/capy/8.examples/8c.buffer-composition.html) | Composing buffer sequences without allocation for scatter/gather I/O | +| `mock-stream-testing/` | [Mock Stream Testing](https://develop.capy.cpp.al/capy/8.examples/8d.mock-stream-testing.html) | Unit testing protocol code with mock streams and error injection | +| `type-erased-echo/` | [Type-Erased Echo](https://develop.capy.cpp.al/capy/8.examples/8e.type-erased-echo.html) | Echo server demonstrating the compilation firewall pattern | +| `timeout-cancellation/` | [Timeout with Cancellation](https://develop.capy.cpp.al/capy/8.examples/8f.timeout-cancellation.html) | Racing an operation against a deadline, and cancelling it with a stop token | +| `parallel-fetch/` | [Parallel Fetch](https://develop.capy.cpp.al/capy/8.examples/8g.parallel-fetch.html) | Running multiple operations concurrently with `when_all` | +| `strand-serialization/` | [Strand Serialization](https://develop.capy.cpp.al/capy/8.examples/8k.strand-serialization.html) | Protecting shared state with a strand instead of a mutex | +| `async-mutex/` | [Async Mutex](https://develop.capy.cpp.al/capy/8.examples/8l.async-mutex.html) | Fair FIFO coroutine locking with `async_mutex` | +| `parallel-tasks/` | [Parallel Tasks](https://develop.capy.cpp.al/capy/8.examples/8m.parallel-tasks.html) | Distributing CPU-bound work across a thread pool | +| `custom-executor/` | [Custom Executor](https://develop.capy.cpp.al/capy/8.examples/8n.custom-executor.html) | Implementing the `Executor` concept with a single-threaded run loop | +| `sender-bridge/` | [Bridging a P2300 Sender](https://develop.capy.cpp.al/capy/8.examples/8o.sender-bridge.html) | Awaiting a `std::execution` sender from a Capy coroutine. Requires `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` and C++23 | +| `asio/` | [Calling Asio from a Capy Coroutine](https://develop.capy.cpp.al/capy/8.examples/8p.asio-use-capy.html) | `use_capy_example.cpp`: Asio operations awaited through a `use_capy` completion token. Requires Boost.Asio | +| | [Using a Capy Stream from Asio](https://develop.capy.cpp.al/capy/8.examples/8t.asio-callbacks.html) | `asio_callbacks.cpp`: a Capy stream driven by Asio completion handlers | +| | [The Dimovian Ideal](https://develop.capy.cpp.al/capy/9.design/9n.WhyNotCobalt.html#_the_dimovian_ideal) | `any_stream.cpp`: stream algorithms written with no Asio headers | +| `gui-integration/` | [GUI Integration](https://develop.capy.cpp.al/capy/8.examples/8q.gui-integration.html) | Running a Capy coroutine on a GUI event loop | +| `early-quitter/` | [Writing Your Own Task Type (Early Quitter)](https://develop.capy.cpp.al/capy/8.examples/8r.early-quitter.html) | A second task type, whose coroutines a stop ends at their current `co_await` with no checks | + +## Other programs + +These programs have no page of their own. The +[Other Programs in the Repository](https://develop.capy.cpp.al/capy/8.examples/8.intro.html#other-programs-in-the-repository) +table in the manual lists them too. + +| Directory | What it is | How it is built | +| --- | --- | --- | +| `awaitable-sender/` | A Capy awaitable wrapped as a `stdexec` sender, the reverse of `sender-bridge/` | `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` and C++23; fetches NVIDIA stdexec. Test: `capy_example_awaitable_sender_test` | +| `any-sender-size/` | Measures the `exec::any_sender` operation state and the heap allocation its `connect` performs | The same flag as `awaitable-sender/`. Test: `capy_example_any_sender_size` | +| `fabrics/` | Compile-only listings from P4251R0 for HPC fabric libraries | Default build; each fabric check builds when its library is found | +| `cuda/batched-write/` | A batched `write_some` to a CUDA device stream | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and the CUDA toolkit. Test: `capy_example_cuda_batched_write` | +| `cuda/datamovement/` | Compile-only CUDA data-movement listings from P4251R0 | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` | +| `cuda/notification-strategies/` | One GPU completion, awaited in three different ways | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` | +| `cuda/pipeline/` | Capy tasks and senders on the `stream_scheduler` of NVIDIA `nvexec` | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` plus `BOOST_CAPY_BUILD_NVEXEC_EXAMPLES=ON`, which needs `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON` and C++23 | +| `echo-server-corosio/` | An echo server on Corosio | CMake when a `Boost::corosio` target exists, or its own Jamfile | +| `when-any-cancellation/` | A race of three data sources with `when_any`. **Out of date:** its comments say that the first child to complete wins, and its losing children report success. For the current rules, see [The canceled Convention](https://develop.capy.cpp.al/capy/4.coroutines/4f.cancellation.html#_the_canceled_convention) and [Timeout with Cancellation](https://develop.capy.cpp.al/capy/8.examples/8f.timeout-cancellation.html) | Default build, or B2 | ## Building @@ -66,3 +64,5 @@ cmake --build build ```bash b2 example ``` + +B2 builds only the directories listed in `example/Jamfile`. From 602a5130e8b44b3a3b122f354635c2a8e7224750 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 04:37:19 +0100 Subject: [PATCH 05/12] docs: tighten wording on the new example and primitives pages - 4l: the lock error code covers every cancellation, the shutdown callout no longer relies on a declaration outside the include, and the allocation rule notes that posting a waiter may allocate. - 8r: place the asynchronous stop race before the gate runs, say that the shutdown output omits its latency line, and drop the note that went beyond the two ideas kept from doc/quitter.md. - 8.intro: link any_stream.cpp itself, and do not link the out-of-date when-any-cancellation directory. - 8p: link the 9n section on any_stream.cpp from See Also. - example/README.md: any-sender-size counts the allocations of both connect and start. --- .../ROOT/pages/4.coroutines/4l.synchronization.adoc | 7 ++++--- doc/modules/ROOT/pages/8.examples/8.intro.adoc | 4 ++-- .../ROOT/pages/8.examples/8p.asio-use-capy.adoc | 1 + .../ROOT/pages/8.examples/8r.early-quitter.adoc | 13 +++---------- example/README.md | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc index dd4dab386..99c14cc34 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -61,7 +61,7 @@ include::example$async-mutex/async_mutex.cpp[tag=scoped_lock,indent=0] ---- `scoped_lock()` completes with an error code and a guard. -The error code is set when a stop request arrives while the task waits in the queue. +The error code is set when the lock is canceled, for example by a stop request that arrives while the task waits in the queue. The guard releases the mutex when it leaves its scope. In this example, the critical section does not suspend. @@ -165,7 +165,7 @@ include::example$snippets/4j_lifetimes.cpp[tag=shutdown_order,indent=0] ---- <1> The waiting task observes the stop token and finishes through its own code. <2> `join` waits for the outstanding work, then for the worker threads. -<3> The pool, and then `never_set`, are destroyed with no waiter left. +<3> From here on, nothing waits on `never_set`, so the event and the pool can be destroyed safely. Destroying a primitive that still has waiters cancels them. Each waiter is posted to its executor to resume with cpp:error::canceled[error::canceled]. @@ -183,7 +183,8 @@ xref:4.coroutines/4j.lifetimes.adoc#_shutdown_ordering[Shutdown Ordering] explai == Allocation and Lifetime -No operation on these primitives allocates memory. +The primitives allocate nothing themselves. +Resuming a waiter posts it to its executor, and that executor may allocate. Each waiter lives in the frame of the task that awaits. The primitives can be neither copied nor moved, because suspended waiters point into them. diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index a540483f4..bee8c6d89 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -72,7 +72,7 @@ Some need an optional compiler or library, and some only check that code compile |=== | Directory | What it is | How it is built -| https://github.com/cppalliance/capy/tree/develop/example/asio[`asio/any_stream.cpp`] +| https://github.com/cppalliance/capy/blob/develop/example/asio/any_stream.cpp[`asio/any_stream.cpp`] | Stream algorithms written against a header with no Asio includes. xref:9.design/9n.WhyNotCobalt.adoc#_the_dimovian_ideal[The Dimovian Ideal] walks through it. | The `capy_example_any_stream` target, when a `Boost::asio` target exists, or the `b2` target `any_stream` @@ -115,7 +115,7 @@ That flag needs `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON` and C++23. | An echo server on Corosio | The default build, when a `Boost::corosio` target exists, or its own `Jamfile` -| https://github.com/cppalliance/capy/tree/develop/example/when-any-cancellation[`when-any-cancellation/`] +| `when-any-cancellation/` | A race of three data sources with cpp:when_any[]. *Out of date:* its comments say that the first child to complete wins, and its losing children report success. For the current rules, see xref:4.coroutines/4f.cancellation.adoc#_the_canceled_convention[The canceled Convention] and xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation]. 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 2bd176523..f08576ba5 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 @@ -53,3 +53,4 @@ example complete! * xref:4.coroutines/4e.context-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` xref:glossary.adoc#sender[senders] * xref:8.examples/8t.asio-callbacks.adoc[Using a Capy Stream from Asio] — the other direction, a Capy stream driven by Asio completion tokens +* xref:9.design/9n.WhyNotCobalt.adoc#_the_dimovian_ideal[The Dimovian Ideal] — `any_stream.cpp`, stream algorithms written with no Asio headers diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index 4892b4fc1..cc8c41347 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -227,14 +227,6 @@ sequenceDiagram R->>K: destroy the top frame, then the frames below it ---- -[NOTE] -.Why not an exception -==== -The quitter that Capy used to contain ended a stopped body by throwing an internal exception. -In C++, a throw is the only way to reach `final_suspend` from a suspension point. -This example never resumes the body, so it needs no exception and no support from the library. -==== - === Results That Race a Stop `transform_awaiter::await_ready` and `transform_awaiter::await_suspend` decide what a stop does to an operation. @@ -248,7 +240,7 @@ Nothing is delivered, and the run stops. *A stop arrives while the operation completes.* The stop can arrive inside the readiness check of the operation. -It can also arrive after the operation resumes the gate, but before the gate decides. +It can also arrive after the operation completes, but before the gate runs. In both cases, `stops_on_completion` takes the result early: * A `canceled` result ends the quitter. @@ -369,7 +361,8 @@ The stop releases its output file and its worker guard, and nothing more is stor In this run, every `released` line came before `Shutdown complete.`, but another run can print some of them after it. For code that reads state after shutdown, copy the order in `shutdown.cpp`: `ticker.join()`, then `pool.join()`, then read the state. -`capy_example_early_quitter_shutdown` prints a summary after the cleanup: +`capy_example_early_quitter_shutdown` prints a summary after the cleanup. +The block leaves out its last line, which reports the shutdown latency of that run: [role=output] ---- diff --git a/example/README.md b/example/README.md index 1c14c608b..e7c2b5637 100644 --- a/example/README.md +++ b/example/README.md @@ -41,7 +41,7 @@ table in the manual lists them too. | Directory | What it is | How it is built | | --- | --- | --- | | `awaitable-sender/` | A Capy awaitable wrapped as a `stdexec` sender, the reverse of `sender-bridge/` | `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` and C++23; fetches NVIDIA stdexec. Test: `capy_example_awaitable_sender_test` | -| `any-sender-size/` | Measures the `exec::any_sender` operation state and the heap allocation its `connect` performs | The same flag as `awaitable-sender/`. Test: `capy_example_any_sender_size` | +| `any-sender-size/` | Measures the `exec::any_sender` operation state and the heap allocations made by `connect` and `start` | The same flag as `awaitable-sender/`. Test: `capy_example_any_sender_size` | | `fabrics/` | Compile-only listings from P4251R0 for HPC fabric libraries | Default build; each fabric check builds when its library is found | | `cuda/batched-write/` | A batched `write_some` to a CUDA device stream | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and the CUDA toolkit. Test: `capy_example_cuda_batched_write` | | `cuda/datamovement/` | Compile-only CUDA data-movement listings from P4251R0 | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` | From 695a306fff066b119013e44d3eb9cdf7f3a7fd7f Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 04:48:48 +0100 Subject: [PATCH 06/12] docs: correct the const-result limitation on the quitter page carries_stop has no cv-qualified forms, so a const result cannot carry the stop and is dropped like a reference result; it is not moved. Also: 4l no longer suggests waking a waker from a signal handler, 8r says to copy shutdown.cpp's order instead of calling main.cpp's order safe, 8t says capy_streams.hpp declares rather than defines, and the CUDA rows name the C++20 requirement. --- doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc | 2 +- doc/modules/ROOT/pages/8.examples/8.intro.adoc | 2 +- doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc | 5 +++-- doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc | 2 +- example/README.md | 6 +++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc index 99c14cc34..41271284e 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -102,7 +102,7 @@ A `wait()` on a set event completes at once, so the order of `set()` and `wait() == Resuming a Task from Another Thread An cpp:async_waker[] connects a task to an event that Capy does not see. -That event can be a timer thread, a signal, or a callback from another library. +That event can come from a timer thread, a callback from another library, or a thread that watches a flag set by a signal handler. One task suspends in `wait()`, and any thread calls cpp:async_waker::wake[wake]`()`. [source,cpp] diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index bee8c6d89..7ebed970d 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -95,7 +95,7 @@ Each fabric check builds when the build finds its library. | https://github.com/cppalliance/capy/tree/develop/example/cuda/batched-write[`cuda/batched-write/`] | A batched `write_some` to a CUDA device stream -| `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and the CUDA toolkit. +| `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON`, C++20, and the CUDA toolkit. Its test is `capy_example_cuda_batched_write`. | https://github.com/cppalliance/capy/tree/develop/example/cuda/datamovement[`cuda/datamovement/`] diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index cc8c41347..4f9b71467 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -277,7 +277,8 @@ After `done.wait()`, it joins the ticker, then calls `pool.join()`, and only the `main.cpp` prints its final message without calling `pool.join()`. Pool threads can still be destroying frames, so `released` lines can follow that message. Only the order of the output depends on this, because `main.cpp` reads no state that the guards update. -Destroying the strands and the wakers before the pool is safe here, and the destructor of the pool joins its threads. +Destroying the strands and the wakers before the pool works here, because the destructor of the pool joins its threads. +Real code should still copy the order in `shutdown.cpp`. [WARNING] ==== @@ -314,7 +315,7 @@ Run them with `ctest --test-dir build -R early_quitter`. * A stop requested before the start still lets the body run to its first `co_await`. * A result that cannot carry the stop is dropped when a stop arrives as the operation completes. - That includes a reference result, and a `const` move-only result is moved. + That includes a reference result and a `const`-qualified result, because `carries_stop` matches neither. * A stopped `results` needs default-constructible `Ts`. * cpp:task[] works around a symmetric-transfer limit of MSVC, and the quitter does not. * The checks do not cover cpp:run[] onto another executor, cpp:when_any[], or a handoff through a strand or a mutex. diff --git a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc index 25848e7a5..570f0c721 100644 --- a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -16,7 +16,7 @@ Drive a Capy cpp:any_stream[] with Asio completion tokens, here plain callbacks. ==== This example is built when a `Boost::asio` target is available, and CI does not build it. The full source is in `example/asio/`: `asio_callbacks.cpp`, and the adapter in `api/uni_stream.hpp`. -Both use `api/capy_streams.hpp`, which defines `asio_context` and `make_stream_pair`. +Both use `api/capy_streams.hpp`, which declares `asio_context` and `make_stream_pair`. ==== == What This Example Shows diff --git a/example/README.md b/example/README.md index e7c2b5637..8d65ff3cc 100644 --- a/example/README.md +++ b/example/README.md @@ -43,9 +43,9 @@ table in the manual lists them too. | `awaitable-sender/` | A Capy awaitable wrapped as a `stdexec` sender, the reverse of `sender-bridge/` | `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` and C++23; fetches NVIDIA stdexec. Test: `capy_example_awaitable_sender_test` | | `any-sender-size/` | Measures the `exec::any_sender` operation state and the heap allocations made by `connect` and `start` | The same flag as `awaitable-sender/`. Test: `capy_example_any_sender_size` | | `fabrics/` | Compile-only listings from P4251R0 for HPC fabric libraries | Default build; each fabric check builds when its library is found | -| `cuda/batched-write/` | A batched `write_some` to a CUDA device stream | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and the CUDA toolkit. Test: `capy_example_cuda_batched_write` | -| `cuda/datamovement/` | Compile-only CUDA data-movement listings from P4251R0 | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` | -| `cuda/notification-strategies/` | One GPU completion, awaited in three different ways | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` | +| `cuda/batched-write/` | A batched `write_some` to a CUDA device stream | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON`, C++20, and the CUDA toolkit. Test: `capy_example_cuda_batched_write` | +| `cuda/datamovement/` | Compile-only CUDA data-movement listings from P4251R0 | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and C++20 | +| `cuda/notification-strategies/` | One GPU completion, awaited in three different ways | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` and C++20 | | `cuda/pipeline/` | Capy tasks and senders on the `stream_scheduler` of NVIDIA `nvexec` | `BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` plus `BOOST_CAPY_BUILD_NVEXEC_EXAMPLES=ON`, which needs `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON` and C++23 | | `echo-server-corosio/` | An echo server on Corosio | CMake when a `Boost::corosio` target exists, or its own Jamfile | | `when-any-cancellation/` | A race of three data sources with `when_any`. **Out of date:** its comments say that the first child to complete wins, and its losing children report success. For the current rules, see [The canceled Convention](https://develop.capy.cpp.al/capy/4.coroutines/4f.cancellation.html#_the_canceled_convention) and [Timeout with Cancellation](https://develop.capy.cpp.al/capy/8.examples/8f.timeout-cancellation.html) | Default build, or B2 | From 7e047fc3f9f59282b26c935234dc284a7279947d Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 04:59:04 +0100 Subject: [PATCH 07/12] docs: give the real reasons main.cpp may destroy its strands early The pool is declared before the strands and wakers, so its joining destructor runs after them and cannot be the reason. The reasons are that each run_async call keeps its own copy of its strand, sharing one reference-counted state, and that a claimed waiter never touches its waker again. Also: the gate is created when an awaited operation is first not ready, 8t says that b2 builds the asio examples unconditionally and that the two chains can interleave differently, and the any-sender-size row matches the README. --- doc/modules/ROOT/pages/8.examples/8.intro.adoc | 2 +- .../ROOT/pages/8.examples/8r.early-quitter.adoc | 10 +++++++--- .../ROOT/pages/8.examples/8t.asio-callbacks.adoc | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index 7ebed970d..87c726241 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -84,7 +84,7 @@ The build fetches NVIDIA stdexec. Its test is `capy_example_awaitable_sender_test`. | https://github.com/cppalliance/capy/tree/develop/example/any-sender-size[`any-sender-size/`] -| A measurement of the operation state and the allocations of `exec::any_sender` +| A measurement of the operation state of `exec::any_sender`, and of the heap allocations made by `connect` and `start` | The same flag as `awaitable-sender/`. Its test is `capy_example_any_sender_size`. diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index 4f9b71467..b791714b7 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -192,7 +192,7 @@ Its `await_suspend` hands the operation the handle of a gate instead of the hand A child quitter receives the same gate handle. The gate is a small coroutine frame of type `gate`. -`make_gate` creates it the first time a quitter suspends, and `~promise_base` destroys it. +`make_gate` creates it the first time an awaited operation is not ready, and `~promise_base` destroys it. The gate loops on `decide_awaiter`, which calls `promise_base::decide` each time an operation resumes the gate. `decide` resumes the body, unless a stop is pending and the result ends the quitter. @@ -276,8 +276,12 @@ After `done.wait()`, it joins the ticker, then calls `pool.join()`, and only the `main.cpp` prints its final message without calling `pool.join()`. Pool threads can still be destroying frames, so `released` lines can follow that message. -Only the order of the output depends on this, because `main.cpp` reads no state that the guards update. -Destroying the strands and the wakers before the pool works here, because the destructor of the pool joins its threads. +`main.cpp` needs no `pool.join()` for this, because only the order of its output lines depends on it. +It reads no state that the guards update. + +The strands and the wakers are destroyed before the pool, and that is safe here for two reasons. +Each cpp:run_async[] call keeps its own copy of its strand, and the copies share one reference-counted state. +A waiter that a wake or a stop already claimed never touches its waker again. Real code should still copy the order in `shutdown.cpp`. [WARNING] diff --git a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc index 570f0c721..d0cf3b489 100644 --- a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -14,7 +14,8 @@ Drive a Capy cpp:any_stream[] with Asio completion tokens, here plain callbacks. [NOTE] ==== -This example is built when a `Boost::asio` target is available, and CI does not build it. +`example/CMakeLists.txt` adds this example only when a `Boost::asio` target is available. +`b2` builds it through `example/Jamfile`, and CI does not build it. The full source is in `example/asio/`: `asio_callbacks.cpp`, and the adapter in `api/uni_stream.hpp`. Both use `api/capy_streams.hpp`, which declares `asio_context` and `make_stream_pair`. ==== @@ -92,6 +93,8 @@ The call returns when both chains end and no work remains. == Output +The two chains run over a local TCP connection, so their lines can interleave differently from run to run. + [role=output] ---- writer: wrote 128 bytes (total 128) From 571db5e0ad099e2a1d0650c6a07846f7d300c94d Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Tue, 10 Nov 2026 05:10:10 +0100 Subject: [PATCH 08/12] docs: note that the CUDA flag also needs the cuda/pipeline flags BOOST_CAPY_BUILD_CUDA_EXAMPLES adds all four cuda/ programs, and cuda/pipeline fails to configure without BOOST_CAPY_BUILD_NVEXEC_EXAMPLES, which needs BOOST_CAPY_BUILD_STDEXEC_EXAMPLES and C++23. Say so under the 8.intro table and in example/README.md. Also: 4l says unlock() skips waiters a stop already claimed and that the shutdown snippet's one-thread pool meets the threading rule; 8r draws the stop transfer from the gate straight to run_async and no longer says inner quitters may use any result type. --- .../ROOT/pages/4.coroutines/4l.synchronization.adoc | 5 +++-- doc/modules/ROOT/pages/8.examples/8.intro.adoc | 7 +++++++ doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc | 4 ++-- example/README.md | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc index 41271284e..9186339ab 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -48,7 +48,7 @@ xref:8.examples/8l.async-mutex.adoc#_strand_vs_async_mutex[Strand vs Async Mutex == Holding a Lock Across a `co_await` A task that finds the cpp:async_mutex[] held suspends instead of blocking its thread. -The waiting tasks form a queue, and `unlock()` posts the first one in it. +The waiting tasks form a queue, and `unlock()` hands the mutex to the first waiter that a stop has not already claimed, then posts it. [source,cpp] ---- @@ -157,7 +157,8 @@ A wakeup that loses the race against a stop request is kept for the next `wait() [#destruction] == Destruction -The usual order is to request a stop, wait for the work to end, and then destroy the primitive: +The usual order is to request a stop, wait for the work to end, and then destroy the primitive. +In this snippet, the pool has one thread, so it meets the rule in <>: [source,cpp] ---- diff --git a/doc/modules/ROOT/pages/8.examples/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index 87c726241..9af7089cd 100644 --- a/doc/modules/ROOT/pages/8.examples/8.intro.adoc +++ b/doc/modules/ROOT/pages/8.examples/8.intro.adoc @@ -121,3 +121,10 @@ That flag needs `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON` and C++23. For the current rules, see xref:4.coroutines/4f.cancellation.adoc#_the_canceled_convention[The canceled Convention] and xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation]. | The default build, or `b2` |=== + +[NOTE] +==== +`BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` adds all four `cuda/` programs at once, `cuda/pipeline` included. +So a build with that flag also needs the `cuda/pipeline` flags, or its configure step fails. +Those flags are `BOOST_CAPY_BUILD_NVEXEC_EXAMPLES=ON` and `BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON`, with C++23. +==== diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index b791714b7..e70daefa3 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -160,7 +160,7 @@ With cpp:task[], every wait would need the check from xref:8.examples/8f.timeout xref:4.coroutines/4f.cancellation.adoc#_checking_the_token[Checking the Token] describes that check. Only `worker` returns `early::quitter`, because it is the top of the run, and its result must carry the stop. -The other three can return any result type, because only another quitter awaits them. +The other three can use a result type that need not carry the stop, because only another quitter awaits them. === Starting the Workers @@ -222,7 +222,7 @@ sequenceDiagram W->>G: post the gate, the result is canceled G->>G: decide(): a stop is pending, and the result is canceled G->>K: stop() walks to the top quitter and stores canceled - K->>R: resume the continuation of the top quitter + G->>R: transfer to the continuation of the top quitter R->>R: the completion handler receives canceled R->>K: destroy the top frame, then the frames below it ---- diff --git a/example/README.md b/example/README.md index 8d65ff3cc..5bb3341a8 100644 --- a/example/README.md +++ b/example/README.md @@ -50,6 +50,11 @@ table in the manual lists them too. | `echo-server-corosio/` | An echo server on Corosio | CMake when a `Boost::corosio` target exists, or its own Jamfile | | `when-any-cancellation/` | A race of three data sources with `when_any`. **Out of date:** its comments say that the first child to complete wins, and its losing children report success. For the current rules, see [The canceled Convention](https://develop.capy.cpp.al/capy/4.coroutines/4f.cancellation.html#_the_canceled_convention) and [Timeout with Cancellation](https://develop.capy.cpp.al/capy/8.examples/8f.timeout-cancellation.html) | Default build, or B2 | +`BOOST_CAPY_BUILD_CUDA_EXAMPLES=ON` adds all four `cuda/` programs at once, +`cuda/pipeline` included. So a build with that flag also needs the +`cuda/pipeline` flags (`BOOST_CAPY_BUILD_NVEXEC_EXAMPLES=ON` and +`BOOST_CAPY_BUILD_STDEXEC_EXAMPLES=ON`, with C++23), or its configure step fails. + ## Building ### CMake From fad9fd9fe671702006c6ce189c1e62134a3cd44e Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Wed, 11 Nov 2026 00:36:11 +0100 Subject: [PATCH 09/12] docs: note that stop_races_wake awaits the waker without a strand --- doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index e70daefa3..a1d5de416 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -312,6 +312,8 @@ Real code should still copy the order in `shutdown.cpp`. * `when_all_branches_stop`: two quitters in the branches of cpp:when_all[] both stop, and the task that awaits cpp:when_all[] resumes. * `stop_races_wake`: a wake and a stop from two threads race 2000 times against a quitter on a thread pool. Each run completes exactly once, and its frame always unwinds. + The check awaits the waker directly on the pool, without a strand, which the xref:4.coroutines/4l.synchronization.adoc#threading-model[threading model] leaves undefined. + It holds only because that one coroutine is the sole waiter, so no two of its continuations can run at once. Run them with `ctest --test-dir build -R early_quitter`. From d355a4aec4268daf493ae4dbad6ca64dddcfab86 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Wed, 11 Nov 2026 00:40:57 +0100 Subject: [PATCH 10/12] docs: style the collapsible block on the asio callbacks page --- doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc index d0cf3b489..7b7b1b399 100644 --- a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -10,6 +10,8 @@ = Using a Capy Stream from Asio :page-mode: how-to +include::partial$diagram-style.adoc[] + Drive a Capy cpp:any_stream[] with Asio completion tokens, here plain callbacks. [NOTE] From a87e7cf571273022d27951c7902b07c874c4fea5 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Wed, 11 Nov 2026 01:20:09 +0100 Subject: [PATCH 11/12] docs: put the author's name in the copyright of the new pages --- doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc | 2 +- doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc | 2 +- doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc index 9186339ab..ea9ff7500 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 TheLingeringWill +// Copyright (c) 2026 Jonathan EL BAZ // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) diff --git a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc index a1d5de416..4eb39da55 100644 --- a/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 TheLingeringWill +// Copyright (c) 2026 Jonathan EL BAZ // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) diff --git a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc index 7b7b1b399..a2349609b 100644 --- a/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 TheLingeringWill +// Copyright (c) 2026 Jonathan EL BAZ // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) From d72c2a5cfa92d8f53ad7d2f726dec3c5fadbd592 Mon Sep 17 00:00:00 2001 From: TheLingeringWill Date: Thu, 12 Nov 2026 01:14:59 +0100 Subject: [PATCH 12/12] docs: require one shared serializing executor per mutex or event --- .../ROOT/pages/4.coroutines/4l.synchronization.adoc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc index ea9ff7500..2fb1851d8 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -128,12 +128,15 @@ Only one task may wait on a waker at a time. == The Threading Model The three primitives share one threading model. -Every task that awaits a primitive must run on an executor that never runs its continuations concurrently. -Use a single-threaded executor, or a strand over a thread pool. -xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] and xref:8.examples/8l.async-mutex.adoc[Async Mutex] use a strand for this reason. +Every task that touches a given mutex or event must run on the same executor, and that executor must never run its continuations concurrently. +Use a single-threaded executor, or one strand over a thread pool that all those tasks share. +Two tasks on two different strands must not share a mutex or an event. +xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] and xref:8.examples/8l.async-mutex.adoc[Async Mutex] run all their tasks on one shared strand for this reason. Awaiting a primitive directly on a multi-threaded executor is undefined. -The exception is `wake()`, which any thread may call. +A waker is the exception. +Only one task waits on it at a time, and that task's executor follows the same rule. +`wake()` may be called from any thread. A primitive resumes each waiter by posting it to its executor. A stop callback can run on any thread, so it also only posts.