Executor Affinity
This page explains where your coroutines execute and how to control execution context.
Code snippets assume using namespace boost::capy; is in effect.
|
The Problem Affinity Solves
When an I/O operation completes, the operating system wakes up some thread. Without affinity tracking, your coroutine might resume on an arbitrary thread:
Thread 1: task starts → co_await read() → suspends
Thread 2: (I/O completes) → task resumes here (surprise!)
This forces you to add synchronization everywhere. Affinity solves this by ensuring coroutines resume on their designated executor.
What is Affinity?
Affinity means a coroutine is bound to a specific executor. When a coroutine
has affinity to executor ex, all of its resumptions occur through ex.
You establish affinity when launching a task:
async_run(ex)(my_task()); // my_task has affinity to ex
How Affinity Propagates
Affinity propagates forward through co_await chains. When a coroutine with
affinity awaits a child task, the child inherits the same affinity:
task<void> parent() // affinity: ex (from async_run)
{
co_await child(); // child inherits ex
}
task<void> child() // affinity: ex (inherited)
{
co_await io.async_read(); // I/O captures ex, resumes through it
}
The mechanism is the affine awaitable protocol: each co_await passes the
current dispatcher to the awaited operation, which stores it and uses it for
resumption.
Flow Diagrams
To reason about where code executes, use this compact notation:
| Symbol | Meaning |
|---|---|
|
Coroutines (lazy tasks) |
|
I/O operation |
|
|
|
Coroutine with explicit executor affinity |
|
Executors |
Changing Affinity with run_on
Sometimes you need a child coroutine to run on a different executor.
The run_on function changes affinity for a subtree:
#include <boost/capy/run_on.hpp>
task<void> parent()
{
// This task runs on ex1 (inherited)
// Run child on ex2 instead
co_await run_on(ex2, child_task());
// Back on ex1 after child completes
}
In flow diagram notation:
!c1 -> c2 -> !c3 -> io
The execution sequence:
-
c1launches onex1 -
c2continues onex1(inherited) -
run_onbindsc3toex2 -
I/O captures
ex2 -
I/O completes →
c3resumes throughex2 -
c3completes →c2resumes throughex1(caller’s executor)
Symmetric Transfer
When a child coroutine completes, it must resume its caller. If both share the same executor, symmetric transfer provides a direct tail call with zero overhead—no executor involvement, no queuing.
The decision logic:
-
Same executor → symmetric transfer (direct jump)
-
Different executors → dispatch through caller’s executor
Symmetric transfer is automatic. The library detects when caller and callee share the same dispatcher (pointer equality) and optimizes accordingly.
Type-Erased Dispatchers
The any_dispatcher class provides type erasure for dispatchers:
#include <boost/capy/affine.hpp>
void store_dispatcher(any_dispatcher d)
{
// Can store any dispatcher type uniformly
d(some_handle); // Invoke through type-erased interface
}
task<T> uses any_dispatcher internally, enabling tasks to work with any
executor type without templating everything.
Legacy Awaitable Compatibility
Not all awaitables implement the affine protocol. For standard library awaitables or third-party types, Capy provides automatic compatibility through a trampoline coroutine.
When await_transform encounters a non-affine awaitable, it wraps it:
// Inside task's await_transform (simplified):
if constexpr (affine_awaitable<A, any_dispatcher>)
return affine_path(a); // Zero-overhead
else
return make_affine(a, ex_); // Trampoline fallback
The trampoline adds one extra coroutine frame but ensures correct affinity. Prefer implementing the affine protocol for performance-critical awaitables.
When NOT to Use run_on
Use run_on when:
-
You need CPU-bound work on a dedicated thread pool
-
You need I/O on a specific context
-
You’re integrating with a library that requires a specific executor
Do NOT use run_on when:
-
The child task should inherit the parent’s executor (just
co_awaitdirectly) -
You’re worried about performance — the context switch cost is already paid by the I/O operation itself
Summary
| Concept | Description |
|---|---|
Affinity |
A coroutine is bound to a specific executor |
Propagation |
Children inherit affinity from parents via |
|
Explicitly binds a child to a different executor |
Symmetric transfer |
Zero-overhead resumption when executor matches |
|
Type-erased dispatcher for heterogeneous executor support |
Next Steps
-
Cancellation — Stop token propagation
-
Executors — The execution model in depth