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
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
3 changes: 1 addition & 2 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
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
31 changes: 16 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
24 changes: 12 additions & 12 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,22 +274,22 @@ 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();
}
----
Expand Down
3 changes: 0 additions & 3 deletions doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,6 @@ This design ensures proper context propagation to all children.
| `<boost/capy/delay.hpp>`
| Asynchronous sleep that suspends instead of blocking the thread

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

| `<boost/capy/timeout.hpp>`
| Race an awaitable against a deadline
|===
Expand Down
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ Coroutine frame allocation is rarely the bottleneck. Profile your application be
| `<boost/capy/ex/frame_allocator.hpp>`
| Frame allocator concept and utilities

| `<boost/capy/ex/frame_alloc_promise.hpp>`
| `<boost/capy/ex/frame_alloc_mixin.hpp>`
| Mixin base for promise types that use the TLS frame allocator

| `<boost/capy/ex/recycling_memory_resource.hpp>`
Expand Down
6 changes: 3 additions & 3 deletions doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ void process(socket& sock)
auto task = [&sock]() -> capy::task<>
{
char buf[1024];
auto [ec, n] = co_await sock.read_some(buffer(buf, sizeof(buf)));
auto [ec, n] = co_await sock.read_some(make_buffer(buf));
}();

run_async(executor)(std::move(task));
Expand Down Expand Up @@ -58,7 +58,7 @@ void process(socket& sock)
auto task = [](socket* s) -> capy::task<>
{
char buf[1024];
auto [ec, n] = co_await s->read_some(buffer(buf, sizeof(buf)));
auto [ec, n] = co_await s->read_some(make_buffer(buf));
}(&sock);

run_async(executor)(std::move(task));
Expand Down Expand Up @@ -126,7 +126,7 @@ auto handler = [&sock]() -> capy::task<>
};

// Lambda 'handler' still exists here
run_and_wait(handler()); // Blocks until coroutine completes
capy::test::run_blocking()(handler()); // Blocks until coroutine completes
// Lambda destroyed after coroutine finishes
----

Expand Down
6 changes: 3 additions & 3 deletions doc/modules/ROOT/pages/5.buffers/5b.types.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ auto buf = make_buffer(sp);
elements—including `std::span` and `boost::span`—in addition to the
sources shown above.

The returned buffer type depends on constness:
The returned buffer type depends on the element constness of the range:

* Non-const containers → `mutable_buffer`
* Const containers, `string_view` → `const_buffer`
* Ranges of mutable elements → `mutable_buffer`
* Ranges of const elements, `string_view`, string literals → `const_buffer`

== Layout Compatibility

Expand Down
10 changes: 5 additions & 5 deletions doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ Note the distinction:

Copies data from one buffer sequence to another:

`buffer_copy` is a function object with a single call operator whose
`at_most` parameter defaults to copying everything available:

[source,cpp]
----
template<MutableBufferSequence Target, ConstBufferSequence Source>
std::size_t buffer_copy(Target const& target, Source const& source);

template<MutableBufferSequence Target, ConstBufferSequence Source>
std::size_t buffer_copy(Target const& target, Source const& source,
std::size_t at_most);
std::size_t buffer_copy(Target const& target, Source const& source,
std::size_t at_most = std::size_t(-1));
----

Returns the number of bytes copied.
Expand Down
Loading
Loading