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
16 changes: 10 additions & 6 deletions doc/as_sender.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ produce - at zero allocation cost.
namespace capy = boost::capy;
namespace ex = beman::execution;

// A Capy IoAwaitable - a 500ms timer
auto sndr = capy::as_sender(capy::delay(500ms));
// A Capy IoAwaitable - a single-slot waker wait
capy::async_waker waker;
auto sndr = capy::as_sender(waker.wait());

// Connect a receiver whose environment carries a Capy executor
auto op = ex::connect(
Expand All @@ -159,12 +160,15 @@ auto op = ex::connect(

// Start the operation - no coroutine frame allocated
ex::start(op);

// A user thread wakes the waiter
waker.wake();
```

The receiver's environment provides the executor and stop token. The
bridge threads them into the `io_env` that the awaitable expects. The
timer fires, the executor resumes the handle, the receiver gets
`set_value()`. Twenty-four bytes of `frame_cb` on the operation state.
That is the entire cost.
bridge threads them into the `io_env` that the awaitable expects. When
`waker.wake()` runs, the executor resumes the handle, the receiver
gets `set_value()`. Twenty-four bytes of `frame_cb` on the operation
state. That is the entire cost.

Welcome to the awaitable universe. The door is open.
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,4 @@ You have seen the dangers—race conditions, deadlocks—and the tools to avoid

Concurrency is challenging. Bugs hide until the worst moment. Testing is hard because timing varies. But the rewards are substantial: responsive applications, full hardware utilization, and elegant solutions to naturally parallel problems.

This foundation prepares you for understanding Capy's concurrency facilities: `thread_pool`, `strand`, `when_all`, and `async_event`. These build on standard primitives to provide coroutine-friendly concurrent programming.
This foundation prepares you for understanding Capy's concurrency facilities: `thread_pool`, `strand`, `when_all`, `async_event`, `async_mutex`, and `async_waker`. These build on standard primitives to provide coroutine-friendly concurrent programming.
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ 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.
For a production implementation of this exact pattern, read the source of `async_waker::wait_awaiter` (xref:reference:boost/capy/async_waker/wait_awaiter.adoc[`async_waker::wait_awaiter`]): it registers a stop callback that posts the resume through the executor, and arbitrates between wakeup and cancellation with a single atomic claim.

[#bridging-a-foreign-awaitable]
== Bridging a Foreign Awaitable
Expand Down
49 changes: 37 additions & 12 deletions doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -364,29 +364,54 @@ NOTE: Capy's built-in I/O awaitables (via Corosio) already use the post-back pat

== Part 9: Patterns

=== Timeout Pattern
=== Racing a Deadline

Capy ships a first-class `timeout()` combinator (`<boost/capy/timeout.hpp>`) that races an `io_result`-returning awaitable against a deadline. The first to complete wins and cancels the other; if the timer fires first, the result carries `cond::timeout`:
A timeout is expressed as a race: the operation you care about against
a deadline. `async_waker` provides the suspension point for both sides
of that race; a user thread supplies the clock:

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

using namespace std::chrono_literals;
struct fetch_channel
{
capy::async_waker fetch_ready;
std::atomic<bool> cancelled{false};
std::string result;
};

task<void> read_with_timeout(socket& sock, mutable_buffer buf)
// One side of the race: completes when the fetch worker thread
// wakes fetch_ready. If the deadline wins first, when_any's stop
// request cancels the wait; flag the worker so it stops early.
capy::io_task<std::string> await_fetch(fetch_channel& ch)
{
auto [ec, n] = co_await capy::timeout(sock.read_some(buf), 50ms);
if (ec == cond::timeout)
auto [ec] = co_await ch.fetch_ready.wait();
if (ec)
{
// deadline elapsed before the read completed
co_return;
ch.cancelled.store(true);
co_return capy::io_result<std::string>{ec, {}};
}
// ... use the n bytes read
co_return capy::io_result<std::string>{{}, std::move(ch.result)};
}

// The other side of the race: completes once whatever plays the
// clock wakes the waker.
capy::io_task<> deadline(capy::async_waker& waker)
{
auto [ec] = co_await waker.wait();
co_return capy::io_result<>{ec};
}
----

The deadline itself is built on `delay()` (`<boost/capy/delay.hpp>`), an awaitable that suspends for a duration and resumes with `cond::canceled` if its stop token is activated. Reach for `timeout()` rather than wiring a timer to a `std::stop_source` by hand.
`when_any(await_fetch(ch), deadline(waker))` returns as soon as either side
wakes; the loser's wait resolves with `cond::canceled`. See
xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation]
for the full runnable demo, including the two `std::thread` objects that
play fetch worker and clock.

When the work you are racing is Corosio I/O rather than coroutine-internal
work, Corosio's own timed operations take the place of the deadline
thread, scheduling against the platform event loop instead of a
`std::this_thread::sleep_for`.

=== User Cancellation

Expand Down
63 changes: 28 additions & 35 deletions doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -268,47 +268,43 @@ task<int> process_all(std::vector<item> const& items)
}
----

=== Asynchronous Sleep
=== Racing a Deadline

`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:
`when_any` is the tool for expressing a deadline race directly. Two waker waits, one completed by the operation itself, the other by whatever plays the clock, race as ordinary `when_any` children:

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

task<> example()
capy::io_task<std::string> await_fetch(fetch_channel& ch)
{
auto [ec] = co_await delay(100ms);
// 100ms have elapsed; other coroutines ran on this thread while we waited
auto [ec] = co_await ch.fetch_ready.wait();
if (ec)
{
ch.cancelled.store(true);
co_return capy::io_result<std::string>{ec, {}};
}
co_return capy::io_result<std::string>{{}, std::move(ch.result)};
}
----

[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. 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]
----
#include <boost/capy/timeout.hpp>

task<> example()
capy::io_task<> deadline(capy::async_waker& waker)
{
auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms);
if (ec == cond::timeout)
{
// deadline expired before read completed
}
auto [ec] = co_await waker.wait();
co_return capy::io_result<>{ec};
}

auto race = [&]() -> capy::task<>
{
auto result = co_await capy::when_any(
await_fetch(fetch_ch),
deadline(deadline_waker));
winner = result.index();
if (result.index() == 1)
fetch_result = std::get<1>(std::move(result));
};
----

`timeout` returns the same `io_result` type as the inner awaitable. On timeout, `ec` is set to `error::timeout` and payload values are default-initialized. Unlike `when_any`, exceptions from the inner awaitable are always propagated and never swallowed by the timer.
`deadline_waker.wake()` can come from any thread, a `std::thread` that sleeps for the deadline, hardware, or a completion callback from another library. See xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] for the full example, including the `fetch_channel` type and both `std::thread` objects that play fetch worker and clock.

For timed I/O against a socket, prefer Corosio's own timed operations: they schedule the deadline on the platform event loop instead of a dedicated thread.

== Implementation Notes

Expand Down Expand Up @@ -343,11 +339,8 @@ 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
| `<boost/capy/ex/async_waker.hpp>`
| Single-waiter notification point for deadlines and other external events
|===

You have now learned how to compose tasks concurrently with `when_all` and `when_any`. In the next section, you will learn about frame allocators for customizing coroutine memory allocation.
Loading
Loading