Cancellation
This page explains how to cancel running coroutines using std::stop_token.
Code snippets assume using namespace boost::capy; is in effect.
|
Cooperative Cancellation
Capy supports cooperative cancellation through std::stop_token. When a task
is launched with stop support, the token propagates through the entire call
chain automatically.
Cooperative means:
-
The framework delivers cancellation requests to operations
-
Operations check the token and decide how to respond
-
Nothing is forcibly terminated
How Stop Tokens Propagate
Stop tokens propagate through co_await chains just like affinity. When you
await a stoppable operation inside a task with a stop token, the token is
forwarded automatically:
task<void> cancellable_work()
{
// If this task has a stop token, it's automatically
// passed to any stoppable awaitables we co_await
co_await some_stoppable_operation();
}
The Stoppable Awaitable Protocol
Awaitables that support cancellation implement the stoppable_awaitable
concept. Their await_suspend receives both a dispatcher and a stop token:
template<typename Dispatcher>
auto await_suspend(
std::coroutine_handle<> h,
Dispatcher const& d,
std::stop_token token)
{
if (token.stop_requested())
{
// Already cancelled, resume immediately
return d(h);
}
// Start async operation with cancellation support
start_async([h, &d, token] {
if (token.stop_requested())
{
// Handle cancellation
}
d(h);
});
return std::noop_coroutine();
}
Implementing a Stoppable Timer
Here is a complete example of a stoppable timer:
struct stoppable_timer
{
std::chrono::milliseconds duration_;
bool cancelled_ = false;
bool await_ready() const noexcept
{
return duration_.count() <= 0;
}
// Affine path (no cancellation)
template<typename Dispatcher>
auto await_suspend(coro h, Dispatcher const& d)
{
start_timer(duration_, [h, &d] { d(h); });
return std::noop_coroutine();
}
// Stoppable path (with cancellation)
template<typename Dispatcher>
auto await_suspend(
coro h,
Dispatcher const& d,
std::stop_token token)
{
if (token.stop_requested())
{
cancelled_ = true;
return d(h); // Resume immediately
}
auto timer_handle = start_timer(duration_, [h, &d] { d(h); });
// Cancel timer if stop requested
std::stop_callback cb(token, [timer_handle] {
cancel_timer(timer_handle);
});
return std::noop_coroutine();
}
void await_resume()
{
if (cancelled_)
throw std::runtime_error("operation cancelled");
}
};
Key points:
-
Provide both
await_suspendoverloads (with and without token) -
Check
stop_requested()before starting work -
Register a
stop_callbackto cancel the underlying operation -
Signal cancellation in
await_resume(typically via exception)
Checking Cancellation Status
Within a coroutine, you can check if cancellation was requested:
task<void> long_running_work()
{
for (int i = 0; i < 1000; ++i)
{
// Periodically check for cancellation
if (/* stop requested */)
co_return; // Exit gracefully
co_await process_chunk(i);
}
}
The mechanism for accessing the stop token depends on your task implementation.
When NOT to Use Cancellation
Use cancellation when:
-
Operations may take a long time
-
Users need to abort operations
-
Timeouts are required
Do NOT use cancellation when:
-
Operations are very short — the overhead is not worth it
-
Operations cannot be interrupted meaningfully
-
You need guaranteed completion
Summary
| Concept | Description |
|---|---|
Cooperative |
Operations check the token and decide how to respond |
Automatic propagation |
Tokens flow through |
|
Concept for awaitables that support cancellation |
|
Register cleanup when cancellation is requested |
Next Steps
-
Executors — Understand the execution model
-
stoppable_awaitable — Reference documentation