Cancellation
This section explains how to cancel running coroutines using stop tokens.
Code examples assume using namespace boost::capy; is in effect.
|
Cooperative Cancellation
Capy supports cooperative cancellation through 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> cancellable_work()
{
auto token = co_await this_coro::stop_token;
while (!token.stop_requested())
{
co_await do_chunk();
}
cleanup();
}
How Stop Tokens Propagate
Stop tokens propagate through co_await chains:
task<void> child()
{
auto token = co_await this_coro::stop_token;
// token is from parent
}
task<void> parent()
{
co_await child(); // child sees our token
}
std::stop_source source;
run_async(ex, source.get_token())(parent());
source.request_stop(); // Both parent and child see this
Checking Cancellation
Use this_coro::stop_token to check status:
task<void> long_running()
{
auto token = co_await this_coro::stop_token;
for (int i = 0; i < 1000; ++i)
{
if (token.stop_requested())
{
// Clean up and exit
cleanup();
co_return;
}
co_await process_item(i);
}
}
this_coro::stop_token never suspends—it returns immediately.
Implementing Stoppable Operations
Custom awaitables can support cancellation:
struct stoppable_timer
{
std::chrono::milliseconds duration_;
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)
{
if (token.stop_requested())
{
cancelled_ = true;
ex.dispatch(h).resume();
return;
}
auto handle = start_timer(duration_, [h, ex]() {
ex.dispatch(h).resume();
});
std::stop_callback cb(token, [handle]() {
cancel_timer(handle);
});
}
void await_resume()
{
if (cancelled_)
throw operation_cancelled();
}
};
Key points:
-
Check
stop_requested()before starting -
Use
stop_callbackto cancel underlying operations -
Signal cancellation in
await_resume()
Stop Propagation in when_all
when_all creates an internal stop source for sibling coordination:
task<void> example()
{
co_await when_all(
might_fail(),
long_running(),
another_task()
);
}
If might_fail() throws:
-
Exception captured
-
when_allrequests stop on internal source -
long_running()andanother_task()seestop_requested() -
Wait for all to complete
-
Rethrow first exception
Parent stop tokens also forward:
std::stop_source source;
run_async(ex, source.get_token())(example());
source.request_stop(); // All children see this
Cleanup on Cancellation
Handle cancellation gracefully:
task<void> with_resource()
{
auto resource = acquire_resource();
auto token = co_await this_coro::stop_token;
try {
while (!token.stop_requested())
co_await process(resource);
} catch (...) {
release_resource(resource);
throw;
}
release_resource(resource);
}
Or use RAII:
task<void> with_raii()
{
ResourceGuard guard(acquire_resource());
auto token = co_await this_coro::stop_token;
while (!token.stop_requested())
co_await process(guard.get());
// guard releases on destruction
}
When NOT to Use Cancellation
Use cancellation when:
-
Operations may take a long time
-
Users need to abort operations
-
Timeouts are required
Avoid cancellation when:
-
Operations are very short (overhead not worth it)
-
Operations cannot be interrupted meaningfully
-
You need guaranteed completion
Summary
| Component | Purpose |
|---|---|
|
Controller that requests stop |
|
Observer that checks if stop was requested |
|
Query current token inside coroutine |
|
Execute code when stop is requested |
|
Sibling cancellation on first error |
Next Steps
-
Concurrent Composition — Cancellation with parallel tasks
-
Synchronization — Coordinate coroutines