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:
run_async(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 run_async)
{
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/ex/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/ex/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.
Querying the Current Executor
Within a coroutine, use co_await get_executor() to retrieve the current
executor that this coroutine is bound to:
task<void> example()
{
executor_ref ex = co_await get_executor();
// ex is the executor this coroutine is bound to
}
The get_executor() function returns a tag that the promise intercepts via
await_transform. This operation never suspends—await_ready() always
returns true.
If no executor was set (e.g., in a default-constructed promise), the returned
executor_ref will be empty (operator bool() returns false).
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 |
|
Retrieve the current executor inside a coroutine |
Symmetric transfer |
Zero-overhead resumption when executor matches |
|
Type-erased dispatcher for heterogeneous executor support |
Next Steps
-
Concurrent Composition — Running multiple tasks in parallel
-
Cancellation — Stop token propagation
-
Strands — Serializing coroutine execution