Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 4 additions & 0 deletions doc/modules/ROOT/pages/4.coroutines/4.intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>] 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

Expand Down
9 changes: 8 additions & 1 deletion doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@

`std::coroutine_handle::destroy()` is the {cpp}20 primitive that frees a xref:glossary.adoc#coroutine-frame[coroutine frame]. It is not a cancellation mechanism. It has the same flaw as forceful thread interruption. The coroutine is torn down with no opportunity to complete pending I/O, release locks, or run RAII destructors in the expected order.

Capy exposes cpp:task::handle[handle]`()` so that Capy's own launchers (cpp:run_async[], cpp:run[]) and custom integrations can dispatch coroutines through xref:glossary.adoc#executor[executors]. Calling `destroy()` on such a handle while the coroutine is being awaited by a parent produces undefined behavior: the parent still holds the frame and will destroy it again. A holder that must give up a suspended coroutine for good, such as an xref:glossary.adoc#execution-context[execution context] being torn down, passes its xref:glossary.adoc#continuation[continuation] to cpp:abandon[] instead, which unwinds the chain from the top on its owner's executor.

Check warning on line 162 in doc/modules/ROOT/pages/4.coroutines/4f.cancellation.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.SimpleTense] Avoid needless future/perfect tense — prefer present simple (style guide C4): 'will '.

The rule:

Expand Down Expand Up @@ -368,9 +368,16 @@
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

Expand Down
8 changes: 5 additions & 3 deletions doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
nothing keeps that string alive.

A cpp:task[] is xref:glossary.adoc#lazy[lazy]: the coroutine does not start until it is awaited or
launched. The `task` object owns the frame. Awaiting a task lends the frame to

Check warning on line 37 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).
the awaiting coroutine, which resumes when it finishes; destroying an unstarted
or finished task destroys its frame.

Expand Down Expand Up @@ -67,9 +67,9 @@
include::example$snippets/4j_lifetimes.cpp[tag=value_param]
----

Tasks are stored more often than it looks. Every launch stores one:

Check warning on line 70 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).
cpp:run_async[] starts the task through its xref:glossary.adoc#executor[executor], which can run it after
the full expression that launched it has ended:

Check warning on line 72 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).

[source,cpp]
----
Expand All @@ -92,7 +92,7 @@
* A reference parameter is fine for a task that is always awaited in the
expression that creates it, such as a helper a coroutine calls and
immediately awaits.
* Anything passed to a launched or stored task must be owned by its frame or

Check warning on line 95 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).
outlive it.

== Lambda Captures
Expand Down Expand Up @@ -134,7 +134,7 @@
== Who Owns a Suspended Coroutine

A chain of coroutines awaiting one another is split into _segments_. A segment
is the run of coroutines started together: the task cpp:run_async[] launches

Check warning on line 137 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).
and everything it awaits, a task cpp:run[] starts on another executor, or one
branch of cpp:when_all[] or cpp:when_any[]. The coroutine that started the
segment is its _owner_. It is suspended above the segment until the
Expand All @@ -142,7 +142,7 @@

While the chain is suspended, whatever it waits on holds its xref:glossary.adoc#continuation[continuation]: an
executor's queue, a xref:glossary.adoc#strand[strand], a mutex, a pending I/O operation. Normally the
holder resumes it. A holder that will never resume it, because it is being torn

Check warning on line 145 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.SimpleTense] Avoid needless future/perfect tense — prefer present simple (style guide C4): 'will '.
down, passes the continuation to cpp:abandon[] instead. The segment's owner
then destroys the segment from its top frame down, on its own executor, and
passes the unwind to the segment above. Nothing in an abandoned chain is
Expand Down Expand Up @@ -179,9 +179,11 @@
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:

Expand Down Expand Up @@ -213,7 +215,7 @@
| Task parameter
| By value, unless every caller awaits the task in the same expression.

| Launched or stored task

Check warning on line 218 in doc/modules/ROOT/pages/4.coroutines/4j.lifetimes.adoc

View workflow job for this annotation

GitHub Actions / Antora Docs

[vale_adoc Capy.Terminology] Use 'start' for one-term-per-concept consistency (style guide C.1).
| Everything it reads is owned by its frame or outlives it.

| Lambda coroutine
Expand Down
207 changes: 207 additions & 0 deletions doc/modules/ROOT/pages/4.coroutines/4l.synchronization.adoc
Original file line number Diff line number Diff line change
@@ -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 <<threading-model,The Threading Model>>:

[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
82 changes: 81 additions & 1 deletion doc/modules/ROOT/pages/8.examples/8.intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
====
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading