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(any_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(
any_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, use co_await get_stop_token() to retrieve the current
stop token and check if cancellation was requested:
task<void> long_running_work()
{
auto token = co_await get_stop_token();
for (int i = 0; i < 1000; ++i)
{
if (token.stop_requested())
co_return; // Exit gracefully
co_await process_chunk(i);
}
}
The get_stop_token() function returns a tag that the promise intercepts via
await_transform. This operation never suspends—await_ready() always
returns true.
If no stop token was propagated to this coroutine, the returned token has
stop_possible() == false.
Supporting get_stop_token in Custom Coroutines
If you define your own coroutine type and want to support get_stop_token(),
inherit your promise type from stop_token_support:
struct my_task
{
struct promise_type : stop_token_support<promise_type>
{
my_task get_return_object();
std::suspend_always initial_suspend() noexcept;
std::suspend_always final_suspend() noexcept;
void return_void();
void unhandled_exception();
};
std::coroutine_handle<promise_type> h_;
// ... awaitable interface ...
};
The mixin provides:
-
Storage for the stop token (private)
-
set_stop_token(token)— call from your stoppableawait_suspend -
stop_token()— getter for internal use -
await_transform— interceptsget_stop_token()calls
If you need custom awaitable transformation, override transform_awaitable
instead of await_transform:
struct promise_type : stop_token_support<promise_type>
{
any_dispatcher ex_;
template<typename A>
auto transform_awaitable(A&& a)
{
return make_affine(std::forward<A>(a), ex_);
}
};
To make your coroutine type satisfy stoppable_awaitable (receive tokens when
awaited), implement the stoppable await_suspend overload:
template<dispatcher D>
any_coro await_suspend(any_coro cont, D const& d, std::stop_token token)
{
h_.promise().set_stop_token(token);
// ... rest of suspend logic ...
}
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 |
|
Retrieve the current stop token inside a coroutine |
|
CRTP mixin for custom coroutines to support |
|
Concept for awaitables that support cancellation |
|
Register cleanup when cancellation is requested |
Next Steps
-
Executors — Understand the execution model
-
API Reference — Full reference documentation