The Stop Token
This section explains how cancellation propagates through coroutine chains
using std::stop_token.
Cooperative Cancellation
Capy supports cooperative cancellation through C++20’s std::stop_token:
-
Cooperative: Operations check the token and decide how to respond
-
Non-preemptive: Nothing is forcibly terminated
-
Propagating: Tokens flow automatically through
co_awaitchains
task<void> long_operation()
{
for (int i = 0; i < 1000; ++i)
{
auto token = co_await this_coro::stop_token;
if (token.stop_requested())
co_return; // Exit gracefully
co_await do_chunk(i);
}
}
The Stop Token API
C++20 provides three related types:
#include <stop_token>
std::stop_source source; // Can request stop
std::stop_token token = source.get_token(); // Can check stop
source.request_stop(); // Request cancellation
bool stopped = token.stop_requested(); // Check if requested
bool possible = token.stop_possible(); // Can stop ever be requested?
// Register callback
std::stop_callback cb(token, []() {
std::cout << "Stop requested!\n";
});
stop_source
The controller. Call request_stop() to signal cancellation:
std::stop_source source;
// Later...
source.request_stop(); // All tokens see this
How Stop Tokens Propagate
Stop tokens flow forward through co_await, just like executors:
task<void> child()
{
// Inherits parent's stop token
auto token = co_await this_coro::stop_token;
if (token.stop_requested())
co_return;
}
task<void> parent()
{
// Our stop token is passed to child
co_await child();
}
// Launch with stop token
std::stop_source source;
run_async(pool.get_executor(), source.get_token())(parent());
// Later: cancel everything
source.request_stop();
Querying the Stop Token
Inside a coroutine, retrieve the current stop token:
task<void> cancellable_work()
{
auto token = co_await this_coro::stop_token;
while (!token.stop_requested())
{
co_await do_work();
}
// Cleanup before returning
cleanup();
co_return;
}
this_coro::stop_token never suspends—it’s intercepted by await_transform and
returns immediately.
Implementing Stoppable Awaitables
Custom awaitables can support cancellation:
struct stoppable_timer
{
std::chrono::milliseconds duration_;
timer_handle handle_;
bool cancelled_ = false;
bool await_ready() const
{
return duration_.count() <= 0;
}
void await_suspend(std::coroutine_handle<> h,
executor_ref ex,
std::stop_token token)
{
// Check if already cancelled
if (token.stop_requested())
{
cancelled_ = true;
ex.dispatch(h).resume();
return;
}
// Start timer
handle_ = start_timer(duration_, [h, ex]() {
ex.dispatch(h).resume();
});
// Cancel timer if stop requested
if (token.stop_possible())
{
// Note: stop_callback must outlive the operation
// In real code, store in coroutine frame or awaiter
std::stop_callback cb(token, [this]() {
cancel_timer(handle_);
});
}
}
void await_resume()
{
if (cancelled_)
throw operation_cancelled();
}
};
Key points:
-
Check
stop_requested()before starting work -
Register
stop_callbackto cancel underlying operations -
Signal cancellation in
await_resume()(typically via exception)
Stop Propagation in when_all
when_all creates its own stop source for child coordination:
task<void> example()
{
co_await when_all(
might_fail(),
long_running(),
another_task()
);
}
If any child throws:
-
Exception is captured
-
when_allrequests stop on its internal source -
Sibling tasks see
stop_requested() == true -
when_allwaits for all children to complete -
First exception is rethrown
Parent stop tokens are also forwarded:
std::stop_source source;
run_async(ex, source.get_token())(example());
source.request_stop(); // All children in when_all see this
Error Handling vs Cancellation
Cancellation is not an error—it’s an expected outcome:
task<std::optional<result>> fetch_with_timeout()
{
auto token = co_await this_coro::stop_token;
try {
auto data = co_await fetch_data();
co_return data;
} catch (operation_cancelled const&) {
// Cancellation is expected
co_return std::nullopt;
}
}
Use std::optional, sentinel values, or error codes to signal
cancellation—reserve exceptions for unexpected failures.
When NOT to Use Cancellation
Use cancellation when:
-
Operations may take a long time
-
Users need to abort operations
-
Timeouts are required
-
Parent tasks complete early
Do NOT use cancellation when:
-
Operations are very short (overhead not worth it)
-
Operations cannot be interrupted meaningfully
-
You need guaranteed completion (e.g., cleanup tasks)
Summary
| Component | Purpose |
|---|---|
|
Controller that can request stop |
|
Observer that can check if stop was requested |
|
Execute code when stop is requested |
|
Query current token inside coroutine |
|
Sibling cancellation on first error |
Next Steps
-
The Allocator — Frame allocation strategy
-
Launching Coroutines — Pass stop tokens to
run_async