Skip to content
C++
Library
since C++20
Basic

std::stop_token / stop_source (C++20)

"C++20 cooperative cancellation primitives: stop_token, stop_source, stop_callback, and jthread integration for safe thread shutdown."

std::stop_tokensince C++20

A non-owning, copyable handle to a shared stop state that allows threads to observe cancellation requests issued through an associated std::stop_source.

Overview

C++20 introduced a first-class cooperative cancellation mechanism through three types in <stop_token>:

  • std::stop_source β€” owns the shared stop state; calls request_stop() to signal cancellation.
  • std::stop_token β€” a cheap, copyable observer; threads poll stop_requested() or register callbacks to react to cancellation.
  • std::stop_callback<CB> β€” RAII wrapper that registers a callable to fire when stop is requested, or immediately if it already was.

The shared stop state is reference-counted. Multiple stop_token and stop_source copies all observe the same state β€” calling request_stop() on any stop_source copy affects every associated token. The state transitions exactly once from not stopped to stopped; it cannot be reset.

std::jthread (C++20, <thread>) embeds a stop source internally. If the callable accepts a std::stop_token as its first argument, jthread forwards one automatically. Its destructor calls request_stop() then join() β€” making jthread the preferred foundation for cancellable threads.


Syntax

cpp
#include <stop_token>   // stop_token, stop_source, stop_callback
#include <thread>       // jthread (C++20)

// --- stop_source ---
std::stop_source src;
std::stop_source src2 = src;                   // shares the same stop state
std::stop_source empty{std::nostopstate};      // no associated state

src.stop_possible()     // true if an associated stop state exists
src.stop_requested()    // true after any copy called request_stop()
src.request_stop()      // returns bool: true only the first time it causes the stop
src.get_token()         // β†’ stop_token bound to this source's shared state

// --- stop_token ---
std::stop_token tok = src.get_token();
tok.stop_possible()     // false for default-constructed (empty) token
tok.stop_requested()    // atomic read β€” safe to call from any thread concurrently

// --- stop_callback (C++20) ---
std::stop_callback cb{tok, callable};
// Destructor deregisters and blocks until any in-progress invocation completes

// --- jthread (C++20) ---
std::jthread t([](std::stop_token st) { /* ... */ });
t.get_stop_source()     // β†’ the embedded stop_source
t.get_stop_token()      // β†’ stop_token from the embedded source
t.request_stop()        // same as t.get_stop_source().request_stop()
// Destructor: request_stop() then join() β€” automatic, always

Examples

Polling in a CPU-Bound Loop

cpp
#include <stop_token>
#include <thread>
#include <print>      // C++23; substitute std::cout if targeting C++20

std::jthread worker([](std::stop_token st) {
    while (!st.stop_requested()) {
        process_next_item();
    }
    std::println("worker: stop received, exiting cleanly");  // C++23
});

// Request stop from the owning thread; jthread destructor also does this:
worker.request_stop();
// jthread destructor joins automatically β€” no explicit join needed

stop_requested() is a sequentially-consistent atomic load. Polling in a tight loop is appropriate for CPU-bound work. For blocking operations, use stop_callback to unblock from outside.

stop_callback β€” Unblocking I/O

The fundamental use case for stop_callback is interrupting a thread stuck in a blocking system call:

cpp
#include <stop_token>
#include <sys/socket.h>

void accept_loop(std::stop_token st, int server_fd) {
    // Register BEFORE entering the blocking call β€” order matters
    std::stop_callback on_stop{st, [server_fd] {
        ::shutdown(server_fd, SHUT_RDWR);  // wakes accept() with EINVAL/EBADF
    }};

    while (!st.stop_requested()) {
        sockaddr_storage addr{};
        socklen_t addrlen = sizeof(addr);
        int client = ::accept(server_fd,
                              reinterpret_cast<sockaddr*>(&addr), &addrlen);
        if (client < 0) break;   // interrupted by shutdown
        handle_client(client);
    }
}

Destructor guarantee: stop_callback's destructor blocks until any concurrently-executing invocation of the callback finishes. If request_stop() fires the callback on another thread at the exact moment the registering thread leaves the scope, the destructor will not race β€” it will wait. This guarantee makes the pattern safe provided the callback does not acquire any lock already held by the registering thread.

condition_variable_any β€” Stop-Aware Wait (C++20)

std::condition_variable_any gained stop-token-aware wait() overloads in C++20. They return false when woken by a stop request, eliminating the need to poll separately after the wait:

cpp
#include <condition_variable>  // condition_variable_any (C++20 overloads)
#include <mutex>
#include <queue>
#include <stop_token>

template<typename T>
class BlockingQueue {
    std::queue<T>               q_;
    mutable std::mutex          mtx_;
    std::condition_variable_any cv_;   // _any required; condition_variable lacks stop overloads

public:
    // Returns false when stop was requested before an item was available
    bool pop(T& out, std::stop_token st) {
        std::unique_lock lock{mtx_};
        bool ok = cv_.wait(lock, st, [this]{ return !q_.empty(); });
        if (!ok) return false;
        out = std::move(q_.front());
        q_.pop();
        return true;
    }

    void push(T val) {
        { std::lock_guard g{mtx_}; q_.push(std::move(val)); }
        cv_.notify_one();
    }
};

std::condition_variable (without _any) does not have stop-aware overloads β€” always use condition_variable_any when integrating with stop_token.

Multiple Workers Sharing One stop_source

Each jthread has its own independent internal stop source. To broadcast cancellation across a pool, share a token from a single stop_source and ignore each thread's built-in token:

cpp
std::stop_source pool_stop;

std::vector<std::jthread> pool;
pool.reserve(4);
for (int i = 0; i < 4; ++i) {
    // Capture shared token by value; shadow the jthread-provided one
    pool.emplace_back([i, tok = pool_stop.get_token()](std::stop_token /*ignored*/) {
        while (!tok.stop_requested()) {
            process_shard(i);
        }
    });
}

// Cancel every worker with a single call:
pool_stop.request_stop();   // returns true on first call, false on retries
// pool's destructor joins all threads

Temporarily Disabling Cancellation

A precise but useful technique: swap the active token with a default-constructed (never-stoppable) one to protect a critical section from interruption, then restore it:

cpp
std::jthread t([](std::stop_token st) {
    begin_phase_one(st);

    {
        std::stop_token no_cancel;    // default-constructed: stop_possible() == false
        std::swap(st, no_cancel);     // cancellation now disabled for this scope

        commit_transaction();         // must not be interrupted

        std::swap(st, no_cancel);     // restore original token
    }

    // Cancellation re-enabled; check immediately in case stop arrived during commit
    while (!st.stop_requested()) {
        begin_phase_two();
    }
});

std::stop_callback in Depth

stop_callback is a RAII type that registers a callable against a stop token and automatically deregisters when it goes out of scope. Its constructor signature is:

cpp
// C++20
template<class CB>
explicit stop_callback(std::stop_token st, CB&& cb);

The callable cb is stored by value inside the stop_callback object. Two things can happen at construction:

  1. If st.stop_requested() is already true at construction time, the callback fires immediately on the constructing thread, before the constructor returns.
  2. Otherwise, the callback is registered and will fire on whichever thread later calls request_stop() on any associated stop source.

The destructor deregisters the callback. If the callback is currently executing on another thread at the moment the destructor runs, the destructor blocks until that execution completes. This is the key safety guarantee: the callback can never be mid-flight when the object that owns its captures is destroyed.

cpp
// C++20
#include <stop_token>
#include <atomic>
#include <iostream>

void demonstrate_stop_callback(std::stop_token st) {
    // Pattern: RAII deregistration is automatic
    {
        std::stop_callback cb1{st, []{ std::cout << "callback 1 fired\n"; }};
        std::stop_callback cb2{st, []{ std::cout << "callback 2 fired\n"; }};

        // Both cb1 and cb2 are registered here.
        // If request_stop() is called before this scope exits,
        // both fire (in unspecified order) on the request_stop() caller's thread.

    } // cb2 deregisters first (LIFO within a scope is not guaranteed β€” they deregister on destruction)

    // cb1 and cb2 are fully deregistered here; no dangling references possible.
}

Multiple callbacks on the same token all fire when stop is requested. The order of invocation is unspecified β€” do not rely on execution order between separate stop_callback objects.

Immediate Firing on Already-Stopped Token

This subtlety catches developers off-guard. If you construct a stop_callback against a token that is already stopped, your callable executes synchronously in the constructor:

cpp
// C++20
std::stop_source src;
src.request_stop();           // already stopped

std::stop_callback cb{
    src.get_token(),
    []{ std::cout << "fires immediately in constructor\n"; }
};
// The line above prints before returning from the constructor.

Best Practice. When a callback's body is non-trivial or acquires locks, verify that immediate firing on the constructing thread is safe. Callbacks written for asynchronous invocation may deadlock if they run synchronously while the constructing thread holds a lock.


jthread Integration

std::jthread is the C++20 answer to std::thread's lack of cancellation and automatic joining. The design difference is stark:

cpp
// C++20 β€” std::thread: manual coordination required
{
    std::atomic<bool> should_stop{false};
    std::thread t([&should_stop] {
        while (!should_stop.load()) { do_work(); }
    });
    should_stop.store(true);
    t.join();   // must not forget this; forgetting calls std::terminate
}

// C++20 β€” std::jthread: built-in stop + automatic join
{
    std::jthread t([](std::stop_token st) {
        while (!st.stop_requested()) { do_work(); }
    });
    // Destructor calls request_stop() then join() β€” nothing else needed
}

Callable Signatures jthread Accepts

jthread accepts any callable. If the first parameter is std::stop_token, the thread's internal stop token is passed automatically. Additional constructor arguments are forwarded as subsequent parameters, as with std::thread:

cpp
// C++20
// No stop_token β€” jthread still auto-joins, but no cancellation path
std::jthread t1([]{ run_forever(); });

// stop_token only
std::jthread t2([](std::stop_token st) {
    while (!st.stop_requested()) { run_step(); }
});

// stop_token + additional args
std::jthread t3([](std::stop_token st, int id, std::string label) {
    while (!st.stop_requested()) { run_worker(id, label); }
}, 42, "primary");

jthread Destructor Behavior

The destructor always does exactly two things, in order:

  1. get_stop_source().request_stop() β€” signals the embedded stop source.
  2. join() β€” waits for the thread to finish.

This means a jthread that is never explicitly stopped will still be stopped cleanly when it goes out of scope. There is no equivalent to the std::thread destructor calling std::terminate on a joinable thread.

cpp
// C++20
void launch_background_indexer() {
    std::jthread indexer([](std::stop_token st) {
        while (!st.stop_requested()) {
            index_next_file();
        }
    });
    // indexer goes out of scope here:
    // 1. request_stop() is called β€” worker exits the while loop
    // 2. join() is called β€” function blocks until worker is done
    // No std::terminate risk, no leaked thread
}

Accessing the Embedded Stop Source

jthread exposes both the stop source and a token directly:

cpp
// C++20
std::jthread t([](std::stop_token st) {
    while (!st.stop_requested()) { work(); }
});

std::stop_source src = t.get_stop_source();  // copy of the embedded source
std::stop_token  tok = t.get_stop_token();   // token from the embedded source

t.request_stop();  // equivalent to t.get_stop_source().request_stop()

Polling vs Callback: When to Use Each

The right approach depends on whether the thread can periodically check a flag or whether it is stuck waiting for an external event.

Polling β€” CPU-Bound Loops

Poll stop_requested() inside any loop where the thread regularly completes iterations:

cpp
// C++20 β€” polling is appropriate here
std::jthread compressor([](std::stop_token st) {
    while (!st.stop_requested()) {
        auto chunk = next_chunk();
        if (!chunk) break;
        compress_and_write(chunk);
    }
});

Polling is simple, zero-overhead per check (one atomic load), and sufficient whenever the granularity of each loop iteration is acceptable as a latency for stop response.

Callback β€” Blocking Waits

When a thread is blocked inside accept(), read(), cv.wait(), or sleep_for(), it cannot poll. A stop_callback fires from the outside to unblock it:

cpp
// C++20 β€” callback is necessary here
#include <stop_token>
#include <thread>
#include <chrono>

std::jthread sleeper([](std::stop_token st) {
    // A naive sleep_for cannot be interrupted β€” the thread stays blocked
    // std::this_thread::sleep_for(std::chrono::hours{1}); // wrong

    // Correct: use stop_token-aware sleep
    std::stop_callback wake_up{st, []{}};  // placeholder; see below

    // Better: condition_variable_any with stop-aware wait
    std::mutex mtx;
    std::condition_variable_any cv;
    std::unique_lock lock{mtx};
    // Wait up to 1 hour, or until stop is requested:
    cv.wait_for(lock, st, std::chrono::hours{1}, []{ return false; });
    // Returns when stop is requested OR timeout expires
});

The condition_variable_any::wait_for overload that accepts a stop_token returns cv_status::no_timeout when the stop fires, interrupting the sleep cleanly.

Combined Pattern

Many real threads both compute and occasionally block. Use both approaches together:

cpp
// C++20
std::jthread processor([](std::stop_token st) {
    BlockingQueue<Work> queue;  // from earlier example

    Work item;
    while (queue.pop(item, st)) {   // blocking wait, stop-aware
        execute(item);              // CPU work β€” stop not checked per-instruction
        // (if execute() is long, add an inner poll loop inside it)
    }
    // pop() returned false: stop was requested
});

Cancelling Blocking I/O with stop_callback

A stop_callback is the standard mechanism for making otherwise-uncancellable blocking I/O responsive to cooperative cancellation. The pattern: register a callback that triggers platform cancellation, then enter the blocking call. When stop is requested, the callback fires on the requester's thread and unblocks the I/O.

Closing a File Descriptor (POSIX)

cpp
// C++20 β€” POSIX
#include <stop_token>
#include <unistd.h>
#include <sys/socket.h>

void read_loop(std::stop_token st, int fd) {
    // Closing fd from the callback will cause read() to return -1 / EBADF.
    // Register BEFORE the first read β€” a request_stop() between the two
    // would otherwise be missed.
    std::stop_callback on_stop{st, [fd] {
        ::close(fd);   // thread-safe: close() is async-signal-safe
    }};

    char buf[4096];
    ssize_t n;
    while ((n = ::read(fd, buf, sizeof(buf))) > 0) {
        process(buf, n);
    }
    // n == -1 with errno == EBADF when cancelled, or 0 on EOF
}

Common Pitfall. Closing the file descriptor from a callback races with the calling thread potentially reopening it (the OS may reuse the descriptor number). In production code, prefer using a self-pipe, eventfd, or io_uring cancellation instead of close() directly. The pattern above is illustrative; platform-specific solutions are more robust.

Platform Cancellation API (Windows)

On Windows, CancelIoEx can cancel pending I/O on a handle without closing it:

cpp
// C++20 β€” Windows
#include <stop_token>
#include <windows.h>

void read_loop_win(std::stop_token st, HANDLE hFile, OVERLAPPED& ov) {
    std::stop_callback on_stop{st, [hFile, &ov] {
        ::CancelIoEx(hFile, &ov);   // cancels the specific overlapped operation
    }};

    DWORD bytes_read{};
    BOOL ok = ::ReadFile(hFile, buf, sizeof(buf), &bytes_read, &ov);
    if (!ok && ::GetLastError() == ERROR_OPERATION_ABORTED) {
        // Cancelled by stop request
    }
}

Thread Pool with Stop Tokens

A minimal thread pool demonstrates how stop tokens integrate into a larger concurrent system. Each worker holds a token from a shared stop source. Shutdown consists of calling request_stop() once and then joining.

cpp
// C++20
#include <stop_token>
#include <thread>
#include <vector>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    explicit ThreadPool(std::size_t n) {
        workers_.reserve(n);
        for (std::size_t i = 0; i < n; ++i) {
            // Each jthread receives the POOL's shared token, not its own.
            // This lets a single request_stop() shut down all workers.
            workers_.emplace_back(
                [this, tok = stop_.get_token()](std::stop_token /*own*/) {
                    worker_loop(tok);
                }
            );
        }
    }

    void submit(std::function<void()> task) {
        {
            std::lock_guard g{mtx_};
            tasks_.push(std::move(task));
        }
        cv_.notify_one();
    }

    void shutdown() {
        stop_.request_stop();   // broadcast to all workers
        cv_.notify_all();       // wake any worker blocked in cv_.wait
        // jthread destructors in workers_ join automatically when ThreadPool is destroyed
    }

    ~ThreadPool() {
        shutdown();
        // workers_ destructor joins each jthread (request_stop already called)
    }

private:
    void worker_loop(std::stop_token st) {
        while (!st.stop_requested()) {
            std::function<void()> task;
            {
                std::unique_lock lock{mtx_};
                // Stop-aware wait: returns false when stop is requested
                bool got_task = cv_.wait(lock, st,
                    [this]{ return !tasks_.empty(); });
                if (!got_task) break;   // stop requested, no task available
                task = std::move(tasks_.front());
                tasks_.pop();
            }
            task();  // Execute outside the lock
        }
    }

    std::stop_source                  stop_;
    std::vector<std::jthread>         workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex                        mtx_;
    std::condition_variable_any       cv_;   // _any for stop-token wait overload
};

The key design choices here:

  • Workers block in cv_.wait(lock, st, predicate) β€” this is a single call that handles both "task arrived" and "stop requested" without any manual polling or extra flags.
  • shutdown() calls request_stop() then notify_all(). The notify is needed because workers blocked in cv_.wait are waiting on the condition variable; the stop token wakeup alone may not signal the condition variable depending on the implementation, so an explicit notify ensures prompt wakeup.
  • The pool uses condition_variable_any, not condition_variable. The stop-token overloads exist only on _any.

std::nostopstate and Ownerless Tokens

std::nostopstate is a tag type used to construct a stop_source that has no associated stop state:

cpp
// C++20
#include <stop_token>

// Default construction: has an associated stop state
std::stop_source normal_src;
assert(normal_src.stop_possible() == true);

// nostopstate: no stop state β€” can never be stopped
std::stop_source no_src{std::nostopstate};
assert(no_src.stop_possible() == false);
assert(no_src.stop_requested() == false);

// A token from a nostopstate source is also ownerless
std::stop_token no_tok = no_src.get_token();
assert(no_tok.stop_possible() == false);

// A default-constructed stop_token is also ownerless
std::stop_token default_tok;
assert(default_tok.stop_possible() == false);

Use Case: Optional Cancellation

nostopstate is useful when a function accepts a stop_token parameter but the caller does not want to provide cancellation. The callee's code is unchanged β€” it polls stop_requested() as normal β€” but the token will never be stopped, so the loop runs to completion:

cpp
// C++20
void process_all(DataSource& src,
                 std::stop_token st = std::stop_source{std::nostopstate}.get_token())
{
    while (src.has_data() && !st.stop_requested()) {
        handle(src.next());
    }
}

// Caller 1: no cancellation desired
process_all(my_source);

// Caller 2: cancellable
std::stop_source src;
std::jthread monitor([&src]{ if (timeout()) src.request_stop(); });
process_all(my_source, src.get_token());

Best Practice. When writing a library function that may or may not need cancellation, accept std::stop_token with a nostopstate default. This is zero-overhead for uncancelled callers (the stop_possible() check short-circuits stop_requested() to always return false) and zero API friction for callers that do need cancellation.

Registering a stop_callback against an ownerless token is also a no-op β€” the callback is never stored and stop_possible() returns false so the registration is skipped immediately:

cpp
// C++20
std::stop_token empty;
std::stop_callback cb{empty, []{ /* never fires */ }};
// cb's constructor returns immediately; nothing is registered

Multiple Stop Sources Sharing One State

stop_source is copyable. Every copy shares the same underlying stop state. Any copy can call request_stop(), and every associated token will observe the change:

cpp
// C++20
std::stop_source src1;
std::stop_source src2 = src1;   // same shared state; not an independent copy

std::stop_token tok = src1.get_token();

src2.request_stop();            // transitions the shared state to stopped

assert(src1.stop_requested() == true);   // src1 sees it
assert(tok.stop_requested()  == true);   // tok sees it
assert(src2.request_stop()   == false);  // second call: already stopped, returns false

request_stop() returns true exactly once β€” the call that actually performs the transition. Subsequent calls return false. This allows multiple producers to call request_stop() safely without coordination: only the first wins, and all others are harmless no-ops.

Token Copies All See the Same State

stop_token copies are equally cheap and all point to the same state:

cpp
// C++20
std::stop_source src;
std::stop_token tok_a = src.get_token();
std::stop_token tok_b = tok_a;   // copy β€” same underlying state
std::stop_token tok_c = tok_b;

src.request_stop();

// All three tokens see the stop simultaneously
assert(tok_a.stop_requested());
assert(tok_b.stop_requested());
assert(tok_c.stop_requested());

Distribute stop_token copies freely. There is no cost to having many β€” they share a reference-counted state and each check is a single atomic load.


Best Practices

  • Prefer jthread over thread for any cancellable thread. Automatic stop + join on destruction eliminates the most common thread-lifetime bugs.
  • Register stop_callback before blocking, never after. A request_stop() call between the blocking entry and the registration is a silent missed interrupt.
  • Keep callbacks short and lock-free where possible. The callback fires on whichever thread calls request_stop(), which may be unrelated to the registering thread. Acquiring a lock already held by the registering thread deadlocks.
  • Use condition_variable_any whenever you pair condition waits with stop_token. Its stop-aware overload handles the wake-or-stop race correctly with no extra code.
  • Check stop_possible() to skip registration. A default-constructed token has stop_possible() == false; registering a stop_callback against it is a no-op, but checking first makes intent explicit.
  • Default parameters to std::nostopstate in library functions that optionally support cancellation. Callers that do not need cancellation pay no overhead; callers that do pass their token naturally.
  • Distribute stop_token copies freely. Each copy is a small reference-counted handle. Passing by value is correct and intentional β€” it is how multiple threads observe the same stop state.

Common Pitfalls

Using each jthread's own token for pool-wide cancellation. Each jthread has an independent stop source. Calling t.request_stop() cancels only that thread. Pass an explicit shared token if you need broadcast semantics.

Registering stop_callback after entering a blocking call. The callback exists to interrupt the block from outside. Registering it after accept() or cv.wait() has already started is too late β€” the interrupt may never arrive.

Holding a lock inside a stop_callback that the registering thread also holds. Because stop_callback's destructor waits for any concurrent invocation, and because the callback fires on the request_stop() caller, a shared mutex between the two creates a classic deadlock.

Using std::condition_variable instead of condition_variable_any. The former has no stop-aware overloads. Mixing it with stop_token requires manual polling and reintroduces a race between predicate check and wait().

Assuming stop_requested() establishes happens-before for payload data. The stop state transitions are sequentially consistent with respect to each other, but your work data has no such guarantee. Use your data's own synchronisation primitives; do not rely on the stop mechanism to order arbitrary loads and stores.

Assuming the callback fires asynchronously. If the token is already stopped at the moment you construct the stop_callback, the callback fires synchronously inside the constructor, on your thread, before the constructor returns. Code that assumes the callback always runs on a different thread will break in this case.

Forgetting to notify_all() after request_stop() in a thread pool. Workers blocked in cv_.wait() are waiting on the condition variable. The stop-token integration will cause wait() to return false, but only after the condition variable is notified. Call cv_.notify_all() after request_stop() to ensure workers wake promptly.


See Also