Skip to content
Open

Work #46

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
3fb4a13
Replace make_affine.hpp with affine.hpp
vinniefalco Dec 31, 2025
7bae570
Add executor_dispatcher adapter for affine protocol integration
vinniefalco Dec 31, 2025
7a7144d
Make executor::post and submit const methods
vinniefalco Dec 31, 2025
d8967b8
Minor formatting in task.hpp initial_suspend
vinniefalco Dec 31, 2025
16684a7
Apply format.md: function names on new line in task.hpp
vinniefalco Dec 31, 2025
fbd83ee
Upgrade task to affine awaitable protocol
vinniefalco Dec 31, 2025
c043166
Fix name collision: rename result member to result_ in task promise
vinniefalco Dec 31, 2025
2434194
Fix: add detail:: prefix to is_task_v usage
vinniefalco Dec 31, 2025
8318db0
Fix await_transform return type consistency
vinniefalco Dec 31, 2025
bb5784d
Use decltype(auto) for await_transform return type
vinniefalco Dec 31, 2025
b67351f
Restructure await_transform to use helper functions
vinniefalco Dec 31, 2025
f5304a2
Simplify await_transform: always use make_affine
vinniefalco Dec 31, 2025
fe4a859
Fix await_transform for HALO compatibility
vinniefalco Dec 31, 2025
4e4782c
Make async_result affine-aware
vinniefalco Dec 31, 2025
793b952
Return system::result from launch to propagate exceptions
vinniefalco Dec 31, 2025
6a52354
Update task.cpp tests for new launch() return type
vinniefalco Dec 31, 2025
0d701ab
Use 'ex' consistently for executor variable names in docs
vinniefalco Dec 31, 2025
417ce2a
Rename executor parameter from 'e' to 'ex' consistently
vinniefalco Dec 31, 2025
eaf8e88
Simplify launch: co_await directly in result_type init-list
vinniefalco Dec 31, 2025
61a770f
Add copydoc for launch(executor, task<void>) overload
vinniefalco Dec 31, 2025
bf7babf
Clean up affine.hpp and remove AI banner comments
vinniefalco Dec 31, 2025
a4909aa
Rename async_result to async_op
vinniefalco Dec 31, 2025
eb86126
Remove unused deferred_operation concept from async_op.hpp
vinniefalco Dec 31, 2025
a8cfcca
Remove affinity propagation code, use set_dispatcher override
vinniefalco Dec 31, 2025
61326f6
Fix template qualification for affine_promise base class
vinniefalco Dec 31, 2025
a7ee547
Add comprehensive executor affinity tests
vinniefalco Dec 31, 2025
8d0a878
Use system::result for task result storage
vinniefalco Dec 31, 2025
466d005
Add comprehensive launch() tests
vinniefalco Dec 31, 2025
c2b3e4f
Fix GCC 15 maybe-uninitialized warning in coroutine frames
vinniefalco Dec 31, 2025
900b5e4
Fix GCC 15 false positive in unhandled_exception
vinniefalco Dec 31, 2025
b8682a2
Replace launch() with spawn() for top-level task execution
vinniefalco Dec 31, 2025
38f94c6
Fix GCC 15 false positive in spawn() result construction
vinniefalco Dec 31, 2025
24daa5f
Fix GCC 15 false positive: separate result construction from handler …
vinniefalco Dec 31, 2025
4a3e8e7
Fix MSVC C4702 unreachable code warnings in tests
vinniefalco Dec 31, 2025
e756f75
Revert compiler warning workarounds
vinniefalco Dec 31, 2025
b3f854d
Fix coroutine tests missing co_return statements
vinniefalco Dec 31, 2025
09b9a8c
Minor formatting
vinniefalco Dec 31, 2025
c0b4ea9
Refactor task<void> to use exception_ptr directly instead of system::…
vinniefalco Dec 31, 2025
6e14e81
Add variant2 MRTC for GCC maybe-uninitialized false positive
vinniefalco Dec 31, 2025
b34e7c8
Add pragmas to suppress GCC 12+ maybe-uninitialized false positives
vinniefalco Dec 31, 2025
01d13dd
Update MRTC to use system::result for GCC warning test
vinniefalco Dec 31, 2025
1a21e0b
Add testGccUninitialized with multiple MRTC variations
vinniefalco Dec 31, 2025
8132b73
Use minimal fire_and_forget coroutine type for MRTC tests
vinniefalco Dec 31, 2025
bff9d20
Apply LLM-friendly refactoring to task.hpp
vinniefalco Dec 31, 2025
00f16c5
Add .cursor to gitignore, remove stray Untitled file
vinniefalco Dec 31, 2025
32461c5
Add execution model research notes (post/dispatch/defer)
vinniefalco Jan 1, 2026
fb74df2
Add N4482 executor notes (Kohlhoff 2015)
vinniefalco Jan 1, 2026
bfc36b5
Add N4242 and N4482 executor proposal documents
vinniefalco Jan 1, 2026
f8be71a
Incorporate N4242 and N4482 into execution.md
vinniefalco Jan 1, 2026
f3ab099
WIP: execution_context design exploration
vinniefalco Jan 1, 2026
3791e8d
Add any-executor performance analysis
vinniefalco Jan 1, 2026
e1fc99b
Add async composition research and executor benchmark
vinniefalco Jan 1, 2026
941842b
Add mock I/O infrastructure for async composition demo
vinniefalco Jan 1, 2026
6f2c0c7
Add async_read_some and async_read composed operations
vinniefalco Jan 1, 2026
edaf942
Add async_think and multi-level composition benchmarks
vinniefalco Jan 1, 2026
b6c43ca
Interleave any_* benchmarks and add bench_co.cpp
vinniefalco Jan 1, 2026
0cfa6c6
Add coroutine-flavored async_io awaiter
vinniefalco Jan 1, 2026
d7aba9c
Add coroutine composed operations: async_read_some, async_read, async…
vinniefalco Jan 1, 2026
12fd2a9
Add coroutine composed operations with work queue executor
vinniefalco Jan 1, 2026
326e21f
Use affine protocol with type-erased executor in task promise
vinniefalco Jan 1, 2026
ebb89e6
Refactor bench_co to use affine.hpp and type-erased dispatch
vinniefalco Jan 1, 2026
2b7ef6c
Add any_dispatcher with ops type-erasure and task with affine protocol
vinniefalco Jan 1, 2026
ae6bed9
Move transform_awaiter inside promise_type to fix nested template
vinniefalco Jan 1, 2026
03297b4
Rename to executor_ref, add queue destructor, fix empty(), task self-…
vinniefalco Jan 1, 2026
f957e14
Wrap bench.cpp internals in anonymous namespace
vinniefalco Jan 1, 2026
ed07231
Add root_task wrapper and async_run for proper executor lifetime
vinniefalco Jan 1, 2026
e854120
Add run_on for executor switching with caller_ex_ for proper resumption
vinniefalco Jan 1, 2026
2443bde
Extract coroutine model to model_coro.hpp
vinniefalco Jan 1, 2026
2708624
Rename think_op/async_think to request_op/async_request
vinniefalco Jan 1, 2026
e685237
Add coroutine benchmarks grouped with callback benchmarks
vinniefalco Jan 1, 2026
eab1924
Remove bench_co.cpp, update model_coro.hpp
vinniefalco Jan 1, 2026
97954c7
Make executor_ref and executor parameters const-correct
vinniefalco Jan 1, 2026
9f71efa
Add is_executor concept, fix executor_ref copy assignment
vinniefalco Jan 1, 2026
da3e75e
Make async_request do 100 iterations of async_read
vinniefalco Jan 1, 2026
695eb1c
Add allocation counting to benchmarks
vinniefalco Jan 1, 2026
b6578ab
Add executor equality comparison to executor_ref
vinniefalco Jan 1, 2026
20ac6fa
Add socket class with reusable state for benchmarks
vinniefalco Jan 1, 2026
2def4e1
Add technical report on coroutine-first async I/O framework
vinniefalco Jan 1, 2026
c3d55fb
Clarify coroutine handle type erasure has zero overhead
vinniefalco Jan 2, 2026
dd2892a
Add frame_allocator and has_frame_allocator concepts
vinniefalco Jan 2, 2026
bbf0561
Add frame allocator support to task::promise_type
vinniefalco Jan 2, 2026
e4c9a94
Implement symmetric transfer for stack-safe coroutine chains
vinniefalco Jan 2, 2026
b47c7e5
Add P3352R3 sender compatibility section to corosio paper
vinniefalco Jan 2, 2026
5dbf224
Add frame_pool with thread-local and global overflow for coroutine fr…
vinniefalco Jan 2, 2026
e3b850b
Add recycling allocator benchmarks for fair callback comparison
vinniefalco Jan 2, 2026
034a5bb
Simplify bench.cpp: remove type-erased callbacks, keep native vs coro…
vinniefalco Jan 2, 2026
79c608d
Update corosio.md: document frame allocator protocol and updated benc…
vinniefalco Jan 2, 2026
1294c71
Make async_read call async_read_some 10x, async_request call async_re…
vinniefalco Jan 2, 2026
2b2e30c
Fix callback benchmarks to match coroutine post counts (apples to app…
vinniefalco Jan 2, 2026
c3dce3a
Fix frame_pool: track block sizes to avoid returning undersized blocks
vinniefalco Jan 2, 2026
e56e442
Fix frame_pool size corruption causing crash in RelWithDebInfo
vinniefalco Jan 2, 2026
a3c5674
Fix callback benchmark to match coroutine structure
vinniefalco Jan 2, 2026
5a47f2b
Fix infinite loop in callback async_read_some
vinniefalco Jan 2, 2026
0749560
Add op_cache for callback operation recycling
vinniefalco Jan 2, 2026
bd51af5
Use global frame pool for all coroutine allocations
vinniefalco Jan 2, 2026
2d35af6
Update
vinniefalco Jan 2, 2026
cf162be
Add 'Why I/O Objects?' rationale to frame allocator section
vinniefalco Jan 2, 2026
29d1e42
Add 'Executor Composition and Placement' section explaining why execu…
vinniefalco Jan 2, 2026
09474ce
Rename corosio.md to coro-first-io.md with paper formatting
vinniefalco Jan 2, 2026
bb85a39
Add P3826 analysis: GPU-first design leaves networking unaddressed
vinniefalco Jan 2, 2026
550dc5f
Reframe paper as networking-first design exploration, expand std::exe…
vinniefalco Jan 2, 2026
f0ac479
Add 'The std::execution Tax' section explaining integration cost
vinniefalco Jan 2, 2026
15f7467
Add explicit discussion of encapsulation tradeoff vs frame-embedded s…
vinniefalco Jan 2, 2026
183edee
Consolidate redundant content and trim paper
vinniefalco Jan 2, 2026
5441b1a
Soften reference count claims to be directionally accurate
vinniefalco Jan 2, 2026
ca0dac0
Minor formatting: let quote stand alone after colon
vinniefalco Jan 2, 2026
317431f
Fix code samples to match implementation
vinniefalco Jan 2, 2026
7996cfe
Remove scratch file
vinniefalco Jan 2, 2026
4b24ea9
Fix P3352 -> P3552 reference in Section 6.2
vinniefalco Jan 2, 2026
fdd6f07
Reorganize sections: merge run_on/async_run, add executor customizati…
vinniefalco Jan 2, 2026
323b313
Reorder Section 1 to lead with problem statement
vinniefalco Jan 2, 2026
0468075
Reduce duplication: consolidate forward/backward context, trim P3826 …
vinniefalco Jan 2, 2026
305b7cd
Add clarifying quip: TCP servers don't run on GPUs
vinniefalco Jan 2, 2026
28ec984
Add P2762 analysis, hyperlinked references, and acknowledgements
vinniefalco Jan 2, 2026
2011c1b
Merge sections 3+4, reduce emphatic language
vinniefalco Jan 2, 2026
d638e61
Refactor bench: rename model_coro.hpp to bench_co.hpp, extract callba…
vinniefalco Jan 2, 2026
7718219
Add MSVC template coroutine limitation note with 2021 bug reference
vinniefalco Jan 2, 2026
be39b33
Unify io_context with dual-dispatch executor for callbacks and corout…
vinniefalco Jan 2, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@
/.cache
/.clangd
/compile_commands.json

/.cursor/
/build_clang/

114 changes: 58 additions & 56 deletions doc/modules/ROOT/pages/coroutines.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Capy provides lightweight coroutine support for C++20, enabling
asynchronous code that reads like synchronous code. The library
offers two awaitable types: `task<T>` for lazy coroutine-based
operations, and `async_result<T>` for bridging callback-based
operations, and `async_op<T>` for bridging callback-based
APIs into the coroutine world.

This section covers the awaitable types provided by the library,
Expand All @@ -37,28 +37,33 @@ A `task` owns its coroutine handle and destroys it automatically.
Exceptions thrown within the coroutine are captured and rethrown
when the result is retrieved via `co_await`.

Tasks support scheduler affinity through the `on()` method, which
binds the task to an executor. When a task has affinity, all
internal `co_await` expressions resume on the specified executor,
ensuring consistent execution context.

The `task<void>` specialization is used for coroutines that perform
work but do not produce a value. These coroutines use `co_return;`
with no argument.

=== async_result
=== async_op

xref:reference:boost/capy/async_result.adoc[`async_result<T>`] bridges traditional callback-based asynchronous
xref:reference:boost/capy/async_op.adoc[`async_op<T>`] bridges traditional callback-based asynchronous
APIs with coroutines. It wraps a deferred operation—a callable that
accepts a completion handler, starts an asynchronous operation, and
invokes the handler with the result.

The key advantage of `async_result` is its type-erased design. The
The key advantage of `async_op` is its type-erased design. The
implementation details are hidden behind an abstract interface,
allowing runtime-specific code such as Boost.Asio to be confined
to source files. Headers that return `async_result` do not need
to source files. Headers that return `async_op` do not need
to include Asio or other heavyweight dependencies, keeping compile
times low and interfaces clean.

Use xref:reference:boost/capy/make_async_result.adoc[`make_async_result<T>()`] to create an `async_result` from any
Use xref:reference:boost/capy/make_async_op.adoc[`make_async_op<T>()`] to create an `async_op` from any
callable that follows the deferred operation pattern.

The `async_result<void>` specialization is used for operations that
The `async_op<void>` specialization is used for operations that
signal completion without producing a value, such as timers, write
operations, or connection establishment. The completion handler
takes no arguments.
Expand Down Expand Up @@ -86,38 +91,38 @@ Use `task` when composing asynchronous operations purely within the
coroutine world. Tasks can await other tasks, forming a tree of
dependent operations.

=== When to use async_result
=== When to use async_op

Return `async_result<T>` from a regular (non-coroutine) function that
Return `async_op<T>` from a regular (non-coroutine) function that
wraps an existing callback-based API. The function does not use
`co_await` or `co_return`; instead it constructs and returns an
`async_result` using `make_async_result<T>()`.
`async_op` using `make_async_op<T>()`.

[source,cpp]
----
async_result<std::size_t> async_read(socket& s, buffer& b)
async_op<std::size_t> async_read(socket& s, buffer& b)
{
return make_async_result<std::size_t>(
return make_async_op<std::size_t>(
[&](auto handler) {
s.async_read(b, std::move(handler));
});
}
----

Use `async_result` at the boundary between callback-based code and
Use `async_op` at the boundary between callback-based code and
coroutines. It serves as an adapter that lets coroutines `co_await`
operations implemented with traditional completion handlers.

=== Choosing between them

* Writing new asynchronous logic? Use `task`.
* Wrapping an existing callback API? Use `async_result`.
* Wrapping an existing callback API? Use `async_op`.
* Composing multiple awaitable operations? Use `task`.
* Exposing a library function without leaking dependencies? Use
`async_result` with the implementation in a source file.
`async_op` with the implementation in a source file.

In practice, application code is primarily `task`-based, while
`async_result` appears at integration points with I/O libraries
`async_op` appears at integration points with I/O libraries
and other callback-driven systems.

== Examples
Expand Down Expand Up @@ -170,12 +175,12 @@ the source file.
#ifndef TIMER_HPP
#define TIMER_HPP

#include <boost/capy/async_result.hpp>
#include <boost/capy/async_op.hpp>

namespace mylib {

// Returns the number of milliseconds actually elapsed
boost::capy::async_result<int>
boost::capy::async_op<int>
async_wait(int milliseconds);

} // namespace mylib
Expand All @@ -191,10 +196,10 @@ async_wait(int milliseconds);

namespace mylib {

boost::capy::async_result<int>
boost::capy::async_op<int>
async_wait(int milliseconds)
{
return boost::capy::make_async_result<int>(
return boost::capy::make_async_op<int>(
[milliseconds](auto handler)
{
// In a real implementation, this would use
Expand All @@ -217,22 +222,22 @@ async_wait(int milliseconds)

=== Void operations

This example shows `task<void>` and `async_result<void>` for
This example shows `task<void>` and `async_op<void>` for
operations that complete without producing a value.

[source,cpp]
----
#include <boost/capy/task.hpp>
#include <boost/capy/async_result.hpp>
#include <boost/capy/async_op.hpp>

using boost::capy::task;
using boost::capy::async_result;
using boost::capy::make_async_result;
using boost::capy::async_op;
using boost::capy::make_async_op;

// Wrap a callback-based timer (void result)
async_result<void> async_sleep(int milliseconds)
async_op<void> async_sleep(int milliseconds)
{
return make_async_result<void>(
return make_async_op<void>(
[milliseconds](auto on_done)
{
// In real code, this would start a timer
Expand All @@ -258,66 +263,63 @@ task<void> run_sequence()
}
----

=== Running a task to completion
=== Spawning tasks on an executor

Tasks are lazy and require a driver to execute. This example
shows a simple synchronous driver that runs a task until it
completes.
Tasks are lazy and require a driver to execute. The `spawn()` function
starts a task on an executor and delivers the result to a completion
handler. This is useful for launching tasks from non-coroutine code
or integrating tasks into callback-based systems.

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

using boost::capy::task;

template<class T>
T run(task<T> t)
{
bool done = false;
t.handle().promise().on_done_ = [&done]{ done = true; };
t.handle().resume();

// In a real application, this would integrate with
// an event loop rather than spinning
while (!done)
{
// Process pending I/O events here
}

return t.await_resume();
}
using boost::capy::executor;
using boost::capy::spawn;

task<int> compute()
{
co_return 42;
}

int main()
void start_computation(executor ex)
{
int result = run(compute());
return result == 42 ? 0 : 1;
// Spawn a task on the executor with a completion handler
spawn(ex, compute(), [](auto result) {
if (result.has_value())
std::cout << "Result: " << *result << std::endl;
else
std::cerr << "Error occurred\n";
});
}
----

The `spawn()` function takes an executor, a task, and a completion handler.
The handler receives `system::result<T, std::exception_ptr>` which holds
either the task's return value or any exception thrown during execution.
The task runs to completion on the executor with proper scheduler affinity.

=== Complete request handler

This example combines tasks and async_result to implement a
This example combines tasks and async_op to implement a
request handler that reads a request, processes it, and sends
a response.

[source,cpp]
----
#include <boost/capy/task.hpp>
#include <boost/capy/async_result.hpp>
#include <boost/capy/async_op.hpp>
#include <string>

using boost::capy::task;
using boost::capy::async_result;
using boost::capy::async_op;

// Forward declarations - implementations use async_result
// Forward declarations - implementations use async_op
// to wrap the underlying I/O library
async_result<std::string> async_read(int fd);
async_result<std::size_t> async_write(int fd, std::string data);
async_op<std::string> async_read(int fd);
async_op<std::size_t> async_write(int fd, std::string data);

// Pure coroutine logic using task
task<std::string> process_request(std::string const& request)
Expand Down
63 changes: 63 additions & 0 deletions doc/research/affine-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Affine Awaitable Protocol (Specification)

Minimal, library-agnostic requirements for propagating scheduler affinity through coroutine `co_await` chains.

## 1. Dispatcher

- A dispatcher is any callable object `D` such that:
- For some promise type `P` (default `void`), `D(h)` is valid where `h` is `std::coroutine_handle<P>`.
- Calling `D(h)` eventually resumes `h` (typically by scheduling it on a specific execution context).
- **Lifetime:** In `await_suspend`, the dispatcher is received by lvalue reference. The callee treats it as *borrowed for the duration of the await* (do not retain past completion). Ownership and lifetime are the caller’s responsibility. The caller may store the dispatcher by value (if owning) or by pointer/reference (if externally owned) but must present it to callees as an lvalue reference.

## 2. Awaitable Requirements (callee role)

An awaitable **participates in the affinity protocol** if it provides an overload:

```cpp
template<class Dispatcher>
auto await_suspend(std::coroutine_handle<> h, Dispatcher& d);
```

Semantics:
- The awaitable must use the dispatcher `d` to resume the caller, e.g. `d(h);`.
- It may run its own work on any context; only resumption must use `d`.

## 3. Task/Promise Requirements (caller role)

A task type (its promise) **propagates affinity** if it:

1) **Stores the caller’s dispatcher.**
- Provides a way to set/hold a dispatcher instance for the lifetime of the promise.

2) **Forwards the dispatcher on every await.**
- In `await_transform`, when awaiting `A`, pass the stored dispatcher to the awaited object via an affine awaiter:
```cpp
// dispatcher_handle: whatever you store for the caller's dispatcher (pointer or reference)
return affine_awaiter{std::forward<Awaitable>(a), dispatcher_handle};
```
- The callee receives it as `Dispatcher&` in `await_suspend(h, d)`.
- If `A` is not affine and you still want affinity, wrap it (e.g. `make_affine(A, dispatcher)`) or reject it (strict mode).

3) **Provides both await paths for its own awaitability.**
- `await_suspend(caller)` (legacy, no dispatcher available).
- `await_suspend(caller, dispatcher)` (affine, dispatcher available) that stores dispatcher + continuation before resuming the task’s coroutine.

In the affine path, `await_suspend(caller, d)` typically:
- Stores the continuation handle (caller) and the dispatcher `d` in the promise, then
- Returns the task’s coroutine handle to start execution, enabling later forwarding (via `await_transform`) and final resumption via the stored dispatcher.

4) **Final resumption uses the dispatcher when set.**
- In `final_suspend`, if a dispatcher is stored, resume the continuation via that dispatcher; otherwise use direct symmetric transfer.

## 4. Propagation Rule

- The dispatcher is set once at the top-level task (e.g., `run_on(ex, task)`).
- Each `co_await` forwards the same dispatcher to the awaited object.
- Any awaited object that implements `await_suspend(h, d)` uses that dispatcher to resume its caller, preserving affinity through arbitrary nesting.
- If an awaited object is non-affine and not adapted, affinity may be lost at that point. A trampoline (e.g., `make_affine`) can restore it at the cost of one allocation per await; strict mode instead rejects non-affine awaits.

## 5. Notes

- The helper types in `affine.hpp` (`affine_promise`, `affine_task`, `affine_awaiter`, `make_affine`) are convenience implementations of this protocol. They are not the protocol itself.
- Zero-allocation across the chain requires awaited objects to be affine (or adapted with a zero-alloc awaiter). Trampoline fallback preserves affinity but allocates once per `co_await`.

Loading
Loading