Understanding ASIO cancellation_state and propagate_on_cancel: A Complete Guide
ASIO's cancellation_state class filters cancellation requests between parent signals and child operations, allowing precise control over propagation using predefined policies like enable_terminal_cancellation or custom filter functions.
The chriskohlhoff/asio library implements a granular cancellation model for asynchronous operations through the cancellation_state mechanism. Unlike simple stop tokens, this system allows intermediate layers to transform, suppress, or forward cancellation requests based on operation-specific requirements. This article examines the implementation in include/asio/cancellation_state.hpp and explains how the propagate_on_cancel filter policies control cancellation flow.
How cancellation_state Works
The cancellation_state class template serves as a bridge between a parent cancellation_slot and the child slot passed to composed operations. It maintains internal state about which cancellation types have been requested while applying transformation filters to incoming and outgoing signals.
Construction and Slot Management
When you construct a cancellation_state, it creates an internal implementation object stored via the parent slot. According to the source in include/asio/cancellation_state.hpp (lines 99–102), the constructor stores a pointer to an internal implementation (impl_). If the parent slot is connected, the implementation is created via slot.emplace<impl<…>>().
The class provides two essential accessors:
slot()returns the childcancellation_slotthat you pass to asynchronous operations (lines 74–78)cancelled()returns thecancellation_type_tbits that have already been propagated to the child
The Filtering Mechanism
The core of propagate_on_cancel behavior lies in the bidirectional filter system defined in lines 109–126. Each cancellation_state maintains two filters:
- In-filter: Transforms the cancellation type received from the parent before storing it
- Out-filter: Transforms the stored cancellation type before emitting it to the child slot
These filters are callable objects with the signature cancellation_type_t(cancellation_type_t). When the parent signal emits a cancellation request, the implementation applies the in-filter to transform the type, stores the result in cancelled_, then applies the out-filter to determine what reaches the child.
The clear(mask) method (lines 80–87) allows removing specific cancellation bits from the stored state, accepting a cancellation_type_t mask that defaults to clearing all bits.
Emission and Propagation Flow
When a parent cancellation_signal emits a cancellation request, the cancellation_state implementation invokes its function call operator. As implemented in lines 173–184, this method:
- Applies the in-filter to the incoming cancellation type
- Updates the internal
cancelled_state using bitwise OR - Applies the out-filter to the result
- Emits the transformed cancellation to the child signal if the result is not
none
This architecture decouples the cancellation origin from handling, allowing each composition layer to decide whether to forward, suppress, or transform requests.
Built-in Propagate-on-Cancel Policies
ASIO provides predefined filter types in include/asio/cancellation_state.hpp that express common propagation policies. These control which cancellation_type_t values flow downstream.
Disable Cancellation
The disable_cancellation filter (lines 42–45) blocks all propagation. Any cancellation request from the parent is transformed to cancellation_type::none, effectively isolating child operations from parent cancellation signals.
Enable Terminal Cancellation
The enable_terminal_cancellation policy (lines 46–48) propagates only terminal cancellation, the most severe form that typically indicates complete abandonment of the operation. This is useful when child operations should only stop during catastrophic shutdowns but continue during normal timeouts.
Enable Partial Cancellation
The enable_partial_cancellation filter (lines 64–69) allows both terminal and partial cancellation types to propagate. Partial cancellation typically indicates a soft stop where operations may complete current work but reject new items.
Enable Total Cancellation
The enable_total_cancellation policy (lines 70–75) propagates all cancellation types: terminal, partial, and total. This provides the most permissive propagation, where any parent cancellation request immediately reaches child operations.
Practical Implementation
Implementing cancellation propagation requires connecting a cancellation_signal to a cancellation_state and passing the resulting child slot to asynchronous operations.
Basic Usage Example
The following example demonstrates total cancellation propagation:
#include <asio.hpp>
#include <iostream>
using asio::cancellation_signal;
using asio::cancellation_state;
using asio::enable_total_cancellation;
int main() {
asio::io_context io_context;
// 1. Create a signal that may be cancelled externally
cancellation_signal sig;
// 2. Build a state that propagates all cancellations downstream
cancellation_state<enable_total_cancellation> st(sig.slot());
// 3. Use the child slot in an async operation
asio::steady_timer timer(io_context);
timer.async_wait(st.slot(), [](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) {
std::cout << "Operation was cancelled\n";
}
});
// 4. Emit cancellation (e.g., timeout or user interrupt)
sig.emit(asio::cancellation_type::total);
io_context.run();
return 0;
}
To restrict propagation to only terminal cancellation, change the template argument:
cancellation_state<asio::enable_terminal_cancellation> st(sig.slot());
// Only terminal cancellation will reach the timer
Custom Filter Policies
You can define custom propagation logic by creating filter functors. For example, to propagate only partial cancellation:
struct only_partial {
asio::cancellation_type_t operator()(asio::cancellation_type_t t) const noexcept {
return (t & asio::cancellation_type::partial)
? t
: asio::cancellation_type::none;
}
};
// Apply custom filter
cancellation_state<only_partial> st(sig.slot());
Custom filters enable domain-specific cancellation semantics, such as converting total cancellation to partial based on operation state or suppressing cancellation during critical sections.
Summary
cancellation_stateacts as a bidirectional filter between parent cancellation signals and child operations, defined ininclude/asio/cancellation_state.hpp- Propagation policies like
enable_total_cancellation,enable_terminal_cancellation, anddisable_cancellationdetermine whichcancellation_type_tvalues flow downstream - Custom filters allow arbitrary transformation of cancellation requests using callables with signature
cancellation_type_t(cancellation_type_t) - The
slot()method provides the childcancellation_slotto pass to asynchronous operations, whilecancelled()queries the current cancellation state - Clearing state via
clear(mask)removes specific cancellation bits, allowing operations to reset cancellation status when handling partial shutdowns
Frequently Asked Questions
What is the difference between cancellation_signal and cancellation_state?
A cancellation_signal owns a slot and can emit cancellation requests to all connected handlers. A cancellation_state sits between a parent slot and child operations, filtering and transforming those requests before they reach the child. While the signal broadcasts, the state controls propagation and maintains cancellation history.
When should I use enable_terminal_cancellation versus enable_total_cancellation?
Use enable_terminal_cancellation when child operations should only abort during complete system shutdowns or fatal errors, ignoring softer cancellation requests like timeouts. Use enable_total_cancellation when you want child operations to respond immediately to any cancellation signal, including partial or total cancellation types. The choice depends on whether the child operation can safely handle intermediate cancellation states.
How do I completely disable cancellation propagation to a child operation?
Pass disable_cancellation as the template argument to cancellation_state. This filter transforms all incoming cancellation types to none, ensuring the child slot never receives cancellation signals regardless of parent activity:
asio::cancellation_state<asio::disable_cancellation> st(parent_slot);
Can I change the cancellation filter after construction?
No, the filter type is fixed at compile time as a template parameter to cancellation_state. However, you can create a new cancellation_state with a different filter and reconnect it to the parent slot if runtime policy changes are required. The filters themselves can be stateful functors that change behavior based on internal state, even though the type remains constant.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →