C++ Stop Tokens and Cooperative Thread Cancellation
This article provides an overview of cooperative thread cancellation
in modern C++, focusing on the mechanisms introduced in C++20:
std::stop_token, std::stop_source, and
std::stop_callback. It explains how these primitives
replace ad-hoc thread signaling approaches, how they integrate
seamlessly with std::jthread, and how to implement clean,
safe cancellation paths in concurrent applications.
The Need for Cooperative Cancellation
Before C++20, the standard library lacked a built-in mechanism to
cancel running threads. Developers relied on manual approaches, such as
passing a shared std::atomic<bool> or using condition
variables. Alternatively, platform-specific APIs like POSIX
pthread_cancel offered forced asynchronous termination,
which often causes resource leaks, corrupted state, and undefined
behavior because it ignores RAII and destructor calls.
C++20 introduced cooperative cancellation via the
<stop_token> header. In cooperative cancellation, a
thread is not violently terminated from the outside. Instead, an
external thread issues a cancellation request, and the executing thread
periodically inspects that request and shuts down gracefully at a safe
execution point.
Key Components of the Stop Token Framework
The cancellation model relies on three core types that share an underlying stop-state:
1. std::stop_source
The std::stop_source object represents the controller
that can issue a stop request. It provides the
request_stop() member function. When called, the shared
state transitions to the "stop requested" state.
2. std::stop_token
The std::stop_token object is a lightweight, copyable
handle passed to the worker thread. It allows the thread to query
whether a stop request has been made via the
stop_requested() function. It cannot trigger a stop itself;
it can only observe.
3. std::stop_callback
The std::stop_callback template allows registration of
an invocable object that runs automatically if a stop request is issued
on the associated std::stop_token. If a stop has already
been requested when the callback is registered, the callback runs
immediately on the current thread.
Integration with
std::jthread
The std::jthread (joining thread) class natively
integrates with the stop token mechanism. When a
std::jthread function accepts a
std::stop_token as its first parameter, the runtime
automatically injects the token associated with the thread’s internal
std::stop_source.
Furthermore, a std::jthread issues a stop request
automatically upon destruction before joining:
#include <iostream>
#include <chrono>
#include <thread>
void worker(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Stop requested. Cleaning up...\n";
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::milliseconds(700));
// Requesting cancellation manually:
t.request_stop();
// t automatically joins on scope exit
return 0;
}Interrupting Blocked Operations
Checking stop_requested() in a loop works well for
CPU-bound tasks, but it fails if a thread is blocked on a
synchronization primitive. To handle this,
std::condition_variable_any was enhanced to accept a
std::stop_token.
When waiting on a std::condition_variable_any, passing a
std::stop_token ensures that the wait returns immediately
if a stop request is issued, waking the thread without waiting for a
notify signal or a timeout:
#include <condition_variable>
#include <mutex>
#include <stop_token>
std::mutex cv_m;
std::condition_variable_any cv;
void cancellable_wait(std::stop_token st) {
std::unique_lock<std::mutex> lock(cv_m);
// Returns true if predicate passes, or false if stopped
bool success = cv.wait(lock, st, [] { return false; });
if (!success) {
// Exited due to stop request
}
}Summary of Advantages
- Thread Safety: The underlying state transitions are thread-safe and atomic.
- RAII-Compliant: Thread cancellation respects stack unwinding and deterministic destructor calls.
- Standardized Interface: Replaces fragmented, non-portable implementations with a unified, standard C++ mechanism.
- Low Overhead: Queries against a
std::stop_tokeninvolve lightweight atomic reads when no callbacks or complex state transitions are triggered.