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: 2 additions & 2 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ codecov:
after_n_builds: 1
wait_for_ci: yes

# Make coverage checks informational (report but never fail CI)
coverage:
status:
project:
default:
informational: true
target: auto
threshold: 0%
patch:
default:
informational: true
Expand Down
1 change: 0 additions & 1 deletion .github/compilers.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@
"cxx": "clang++",
"cc": "clang",
"b2_toolset": "clang",
"cxxflags": "-fvisibility=hidden -fvisibility-inlines-hidden",
"is_latest": true
}
]
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,13 @@ jobs:
--warnings-as-errors='*'

- name: FlameGraph
uses: alandefreitas/cpp-actions/flamegraph@v1.9.0
uses: alandefreitas/cpp-actions/flamegraph@v1.9.5
if: matrix.time-trace
continue-on-error: true
with:
source-dir: capy-root
build-dir: capy-root/__build__
github_token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Generate Coverage Report
if: ${{ matrix.coverage && matrix.linux }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The Beast2 family of libraries includes:

Currently, the C++ Standard does not deliver facilities optimized for networking I/O. We believe that Capy should become a standard library component to fill this gap. Our first paper based on Capy introduces the _IoAwaitable_ family of concepts:

- Paper: https://github.com/cppalliance/wg21-papers/blob/master/source/d4003-io-awaitables.md
- Paper: https://github.com/cppalliance/wg21-papers/blob/master/source/_reserve/d4003-io-awaitables.md

## The Beman Way

Expand Down
10 changes: 5 additions & 5 deletions doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ task<> example()
auto t = compute(); // Task created, but "Computing..." NOT printed yet
std::cout << "Task created\n";

int result = co_await t; // NOW "Computing..." is printed
int result = co_await std::move(t); // NOW "Computing..." is printed
std::cout << "Result: " << result << "\n";
}
----
Expand Down Expand Up @@ -167,11 +167,11 @@ task<int> compute();
task<> example()
{
auto t1 = compute();
auto t2 = std::move(t1); // OK: ownership transferred
auto t2 = std::move(t1); // OK: ownership transferred, t1 is now empty

// auto t3 = t2; // Error: task is not copyable
int result = co_await t2; // t1 is now empty

int result = co_await std::move(t2);
}
----

Expand Down
12 changes: 8 additions & 4 deletions doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ int main()
run_async(pool.get_executor())(compute());
// Task is now running on the thread pool

// pool destructor waits for work to complete
return 0;
pool.join(); // wait for outstanding work to complete
}
----

Expand Down Expand Up @@ -72,7 +71,7 @@ Always use the two-call pattern in a single expression.

[source,cpp]
----
// Result handler only (exceptions rethrown)
// Result handler only (an unhandled exception calls std::terminate)
run_async(ex, [](int result) {
std::cout << "Got: " << result << "\n";
})(compute());
Expand All @@ -89,7 +88,12 @@ run_async(ex,
)(compute());
----

When no handlers are provided, results are discarded and exceptions are rethrown (causing `std::terminate` if uncaught).
When no result handler is provided, the result is discarded. An exception
that goes unhandled (no error handler was supplied, or a handler let one
escape) calls `std::terminate`. To react to an error, pass an error handler;
it receives the `std::exception_ptr` and should handle it in place rather
than rethrowing. To catch an error, `co_await` the work inside a coroutine
and use `try`/`catch` rather than launching it fire-and-forget.

== run: Executor Hopping Within Coroutines

Expand Down
30 changes: 21 additions & 9 deletions doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,25 @@ This section explains executors and execution contexts—the mechanisms that con

== The Executor Concept

An *executor* is an object that can schedule work for execution. In Capy, executors must provide three methods:
An *executor* is an object that can schedule work for execution. An executor must be nothrow copy- and move-constructible and provide the following interface:

[source,cpp]
----
concept Executor = requires(E ex, continuation& c) {
{ ex.dispatch(c) } -> std::same_as<std::coroutine_handle<>>;
{ ex.post(c) } -> std::same_as<void>;
{ ex.context() } -> std::convertible_to<execution_context&>;
concept Executor = requires(E const& ce, E const& ce2, continuation& c) {
// Equality comparable
{ ce == ce2 } noexcept -> std::convertible_to<bool>;

// Owning context, returned as an lvalue reference to a type
// derived from execution_context
{ ce.context() } noexcept;

// Work tracking
{ ce.on_work_started() } noexcept;
{ ce.on_work_finished() } noexcept;

// Scheduling
{ ce.dispatch(c) } -> std::same_as<std::coroutine_handle<>>;
{ ce.post(c) };
};
----

Expand Down Expand Up @@ -49,8 +60,9 @@ int main()
{
thread_pool pool;
executor_ref ex = pool.get_executor(); // Type erasure

schedule_work(ex, some_coroutine);

continuation c = /* ... */;
schedule_work(ex, c);
}
----

Expand All @@ -74,8 +86,8 @@ int main()

// Launch work on the pool
run_async(ex)(my_task());
// pool destructor waits for all work to complete

pool.join(); // wait for outstanding work to complete
}
----

Expand Down
33 changes: 18 additions & 15 deletions doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,36 +53,37 @@ This signature receives:
** `env->stop_token` — A stop token for cooperative cancellation
** `env->frame_allocator` — An optional frame allocator

The return type enables symmetric transfer.
Many IoAwaitables return `std::coroutine_handle<>` to enable symmetric transfer, but the concept does not require any particular return type.

== IoAwaitable Concept

An awaitable satisfies `IoAwaitable` if:
An awaitable satisfies `IoAwaitable` if `a.await_suspend(h, env)` is a valid expression:

[source,cpp]
----
template<typename T>
concept IoAwaitable = requires(T& t, std::coroutine_handle<> h, io_env const* env) {
{ t.await_ready() } -> std::convertible_to<bool>;
{ t.await_suspend(h, env) } -> std::same_as<std::coroutine_handle<>>;
t.await_resume();
};
template<typename A>
concept IoAwaitable =
requires(A a, std::coroutine_handle<> h, io_env const* env) {
a.await_suspend(h, env);
};
----

The key difference from standard awaitables is the two-argument `await_suspend` that receives the `io_env`.
The concept constrains only the two-argument `await_suspend` that receives the `io_env`. It does not require `await_ready` or `await_resume`, nor does it constrain the return type of `await_suspend`. A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them.

== IoRunnable Concept

For tasks that can be launched from non-coroutine contexts, the `IoRunnable` concept refines `IoAwaitable` with:
For tasks that can be launched from non-coroutine contexts, the `IoRunnable` concept refines `IoAwaitable` and requires a `promise_type` plus the following:

* `handle()` — Access the typed coroutine handle
* `release()` — Transfer ownership of the frame
* `exception()` — Check for captured exceptions
* `result()` — Access the result value (non-void tasks)
* `handle()`: Access the typed coroutine handle
* `release()`: Transfer ownership of the frame
* `exception()`: Check for captured exceptions (on the promise)
* `result()`: Access the result value (on the promise, for non-void tasks)
* `set_continuation()`: Set the continuation handle (on the promise)
* `set_environment()`: Inject the `io_env` (on the promise)

These methods exist because launch functions like `run_async` cannot `co_await` the task directly. The trampoline must be allocated before the task type is known, so it type-erases the task through function pointers and needs a common API to manage lifetime and extract results.

Context injection methods (`set_environment`, `set_continuation`) are internal to the promise and not part of any concept. Launch functions access them through the typed handle provided by `handle()`.
The context injection methods `set_continuation` and `set_environment` are part of the `IoRunnable` concept: it requires them on the `promise_type`. Launch functions access them through the typed handle provided by `handle()`.

Capy's `task<T>` satisfies this concept.

Expand Down Expand Up @@ -215,6 +216,8 @@ stop_cb_.emplace(env->stop_token, h); // h is a raw coroutine_handle

See xref:4.coroutines/4e.cancellation.adoc#stoppable-awaitables[Implementing Stoppable Awaitables] for a complete example.

For a production implementation of this exact pattern, read the source of `delay_awaitable` (xref:reference:boost/capy/delay_awaitable.adoc[`delay_awaitable`]): it schedules a timer, registers a stop callback that posts the resume through the executor, and arbitrates between the timer and cancellation with a single atomic claim.

== Reference

[cols="1,3"]
Expand Down
31 changes: 14 additions & 17 deletions doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,21 @@ task<> parent()
task<> child()
{
// Receives parent's stop token via IoAwaitable protocol
auto token = co_await get_stop_token(); // Access current token
auto token = co_await this_coro::stop_token; // Access current token
}
----

No manual threading—the protocol handles it.

=== Accessing the Stop Token

Inside a task, use `get_stop_token()` to access the current stop token:
Inside a task, use `co_await this_coro::stop_token` to access the current stop token:

[source,cpp]
----
task<> cancellable_work()
{
auto token = co_await get_stop_token();
auto token = co_await this_coro::stop_token;

while (!token.stop_requested())
{
Expand Down Expand Up @@ -243,8 +243,8 @@ The rule:
----
task<> process_items(std::vector<Item> const& items)
{
auto token = co_await get_stop_token();
auto token = co_await this_coro::stop_token;

for (auto const& item : items)
{
if (token.stop_requested())
Expand All @@ -264,7 +264,7 @@ RAII ensures resources are released on early exit:
task<> with_resource()
{
auto resource = acquire_resource(); // RAII wrapper
auto token = co_await get_stop_token();
auto token = co_await this_coro::stop_token;

while (!token.stop_requested())
{
Expand All @@ -274,34 +274,31 @@ task<> with_resource()
}
----

=== The operation_aborted Convention
=== The canceled Convention

When cancellation causes an operation to fail, the conventional error code is `error::operation_aborted`:
When cancellation causes an operation to fail, the conventional error code is `error::canceled`, which compares equal to the portable condition `cond::canceled`:

[source,cpp]
----
task<std::string> fetch_with_cancel()
{
auto token = co_await get_stop_token();
auto token = co_await this_coro::stop_token;

if (token.stop_requested())
{
throw std::system_error(
make_error_code(std::errc::operation_canceled));
make_error_code(error::canceled));
}

co_return co_await do_fetch();
}
----

== Part 7: OS Integration

Capy's I/O operations (provided by Corosio) respect stop tokens at the OS level:

* *IOCP* (Windows) — Pending operations can be cancelled via `CancelIoEx`
* *io_uring* (Linux) — Operations can be cancelled via `IORING_OP_ASYNC_CANCEL`
When Capy's I/O is provided by Corosio, requesting stop cancels work in progress rather than waiting for the operation to finish on its own. Corosio cancels the pending operation through whatever backend is active for the platform, and it resolves promptly with `std::errc::operation_canceled`.

When you request stop, pending I/O operations are cancelled at the OS level, providing immediate response rather than waiting for the operation to complete naturally.
The mechanism depends on the backend: completion-based backends (Windows IOCP, and io_uring when enabled on Linux) cancel the operation in the kernel, while readiness-based backends (Linux epoll, kqueue on the BSDs and macOS, and the portable select fallback) remove it from the reactor before its system call runs. Either way the operation is reported as cancelled instead of blocking until the I/O would have completed.

[[stoppable-awaitables]]
== Part 8: Implementing Stoppable Awaitables
Expand Down
67 changes: 65 additions & 2 deletions doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,45 @@ task<> example()

The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. Only tasks returning `!ec` can win; errors and exceptions do not count as winning. When a winner is found, stop is requested for all siblings. All tasks complete before `when_any` returns.

For detailed coverage including error handling, cancellation, and the range overload, see Racing Tasks.
=== Errors Do Not Win (wait_for_one_success)

A child that returns a non-zero `ec` (or throws) does *not* win, and it does *not* cancel its siblings. `when_any` keeps waiting until some child succeeds or until every child has finished. Only when *all* children fail does the result settle at index 0, holding an `error_code`.

If you need "complete on the first child to *finish*, success or error," that behavior is opt-in — wrap the child as shown below.

=== Treating an Error as a Win

To make a child win on an error, wrap it so the error becomes a success before `when_any` sees it.

The first pattern translates a specific, benign error into success. Other errors propagate unchanged, so they still do not win:

[source,cpp]
----
// canceled is benign here: translate it to success so when_any picks this child.
io_task<> wrapped()
{
auto [ec] = co_await inner();
if (ec == cond::canceled)
co_return io_result<>{}; // success: when_any sees a winner
co_return io_result<>{ec}; // propagate other errors unchanged
}
----

The second pattern lifts the inner `ec` into the payload. The wrapper always succeeds, so it wins on its first completion, carrying the original error code to the caller:

[source,cpp]
----
// Always succeeds; the winner's payload carries the original ec.
io_task<std::error_code> wrapped()
{
auto [ec] = co_await inner();
co_return io_result<std::error_code>{{}, ec};
}

// when_any(wrapped(), ...) -> variant<error_code, std::error_code, ...>
// index 0: every child failed
// index i: child i won; std::get<i>(result) is its original ec
----

== Practical Patterns

Expand Down Expand Up @@ -228,9 +266,31 @@ task<int> process_all(std::vector<item> const& items)
}
----

=== Asynchronous Sleep

`delay` is the awaitable counterpart to `std::this_thread::sleep_for`. Instead of blocking the thread, it suspends the current coroutine until the duration elapses, leaving the thread free to run other coroutines in the meantime:

[source,cpp]
----
#include <boost/capy/delay.hpp>

task<> example()
{
auto [ec] = co_await delay(100ms);
// 100ms have elapsed; other coroutines ran on this thread while we waited
}
----

[NOTE]
====
A thread is *not* consumed per sleeping coroutine. All concurrently sleeping coroutines on the same execution context share a single timer thread, so a thousand simultaneous `delay()` calls cost one thread, not a thousand.
====

`delay` is cancellable. If the environment's stop token is activated before the deadline, the coroutine resumes early with `ec` set to `error::canceled` (compare with `cond::canceled`); otherwise `ec` is clear. A zero or negative duration completes synchronously without scheduling a timer.

=== Timeout

The `timeout` combinator races an awaitable against a deadline:
The `timeout` combinator races an awaitable against a deadline. It is built directly on `delay` — the inner awaitable is run against a `delay` of the given duration, and whichever completes first cancels the other:

[source,cpp]
----
Expand Down Expand Up @@ -281,6 +341,9 @@ This design ensures proper context propagation to all children.
| `<boost/capy/when_any.hpp>`
| First-completion racing with when_any

| `<boost/capy/delay.hpp>`
| Asynchronous sleep that suspends instead of blocking the thread

| `<boost/capy/timeout.hpp>`
| Race an awaitable against a deadline
|===
Expand Down
Loading
Loading