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_token involve lightweight atomic reads when no callbacks or complex state transitions are triggered.