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/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/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..2fb1851d8 --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc @@ -0,0 +1,207 @@ +// +// 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) +// +// 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()` hands the mutex to the first waiter that a stop has not already claimed, then posts 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 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. +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 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] +---- +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 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. + +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. +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. +In this snippet, the pool has one thread, so it meets the rule in <>: + +[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> 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]. +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 + +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. +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/8.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc index d3d981ae3..9af7089cd 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,76 @@ 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/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` + +| 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 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`. + +| 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`, 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/`] +| 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` + +| `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` +|=== + +[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/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/8.examples/8p.asio-use-capy.adoc b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc index d979997b2..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 @@ -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,5 @@ 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 new file mode 100644 index 000000000..4eb39da55 --- /dev/null +++ b/doc/modules/ROOT/pages/8.examples/8r.early-quitter.adoc @@ -0,0 +1,410 @@ +// +// 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) +// +// 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 use a result type that need not carry the stop, 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 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. + +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 + 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 +---- + +=== 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 completes, but before the gate runs. +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. +`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] +==== +* 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. + 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`. + +== 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`-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. + +== 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. +The block leaves out its last line, which reports the shutdown latency of that run: + +[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 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..a2349609b --- /dev/null +++ b/doc/modules/ROOT/pages/8.examples/8t.asio-callbacks.adoc @@ -0,0 +1,132 @@ +// +// 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) +// +// Official repository: https://github.com/cppalliance/capy +// + += 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] +==== +`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`. +==== + +== 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 + +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) +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 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 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..5bb3341a8 100644 --- a/example/README.md +++ b/example/README.md @@ -2,55 +2,58 @@ 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 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`, 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 | + +`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 @@ -66,3 +69,5 @@ cmake --build build ```bash b2 example ``` + +B2 builds only the directories listed in `example/Jamfile`.