How to use stop tokens in C++20.

Asking a Thread to Stop Instead of Killing It

I spent three days in a high-frequency trading shop chasing a ghost—a thread that refused to die, holding onto a mutex and stalling the entire engine. I had implemented what I thought was a clean shutdown sequence, but I had fundamentally misunderstood how stop tokens in C++20 actually interact with the underlying execution state. Most tutorials treat `std::stop_token` like some magical “kill switch” that forcibly yanks a thread out of its loop, but that’s a lie. If you treat them like a sledgehammer rather than a polite request, you aren’t writing concurrent code; you’re just building a more complex way to leak resources.

I’m not here to walk you through the syntax you can find in any mediocre documentation. I want to talk about the actual mechanics of how these tokens signal intent and, more importantly, where they fail when your logic is too heavy to check them frequently enough. I’m going to show you how to use stop tokens in C++20 to build a deterministic shutdown that won’t leave your system in an indeterminate state when the pressure is on. No fluff, just the rules that actually matter when you’re shipping to production.

Table of Contents

Why Cooperative Cancellation in C Is Your Only Safety Net

Why Cooperative Cancellation in C Is Your Only Safety Net

In the old days—and by that, I mean anything before C++20—we lived in a world of “hard” kills. If a thread was stuck in a loop or waiting on a socket, your only real option was often to let it run until the OS stepped in, or worse, attempt to use platform-specific hacks to forcibly terminate it. We all know how that ends: you kill the thread, but you leave the mutexes locked, the heap in an inconsistent state, and your entire process a ticking time bomb.

This is why cooperative cancellation in C++ isn’t just a feature; it’s a necessity for sanity. Instead of the caller playing executioner, the worker thread is given the agency to see a signal and clean up after itself. By utilizing the std::jthread interruption mechanism, you move the responsibility of shutdown from a blunt instrument to a polite request. The thread checks its state, unwinds its stack, releases its resources, and exits gracefully. It’s the difference between pulling a plug on a running server and sending a `SIGTERM` that allows the database to flush its buffers. If you don’t embrace this pattern, you aren’t writing robust systems; you’re just writing bugs that haven’t crashed your machine yet.

Navigating Stdstop Source and Stdstop Token Usage Without Crashing

The mistake most people make is treating `std::stop_source` and `std::stop_token` usage like a simple boolean flag. It isn’t. If you just poll `token.stop_requested()` in a tight loop, you aren’t doing concurrency; you’re just burning cycles. The real power—and the real danger—lies in how you bridge the gap between the signal and the action. This is where `std::stop_callback` comes in. If you register a callback to a token, you are essentially handing a piece of code to the stop source to execute whenever the state changes. It sounds clean, but if that callback tries to touch anything that isn’t thread-safe, or if it deadlocks against the thread it’s supposed to interrupt, you’ve just built a very expensive brick.

You have to respect the ownership model. A `std::stop_token` is a lightweight handle, but the underlying state is managed by the source. When using the `std::jthread` interruption mechanism, remember that the thread is responsible for checking the token. The language doesn’t magically inject an exception into your execution flow. If your worker thread is stuck in a blocking syscall or a heavy computation that doesn’t periodically check its token, the cancellation will never happen. You’ll end up with a “zombie” thread that refuses to die, holding onto resources long after the rest of the system has moved on.

Five ways to avoid shooting yourself in the foot with stop tokens

  • Stop tokens are cooperative, not preemptive. If your worker thread is stuck in a blocking syscall or a tight loop that doesn’t check `stop_requested()`, the token is just a useless piece of memory. You can’t force a thread to die; you can only politely ask it to leave.
  • Watch your object lifetimes. A common mistake is passing a `std::stop_token` into a lambda that outlives the `std::stop_source`. If the source is destroyed before the thread finishes its check, you aren’t just looking at undefined behavior; you’re looking at a production outage.
  • Don’t ignore the `std::stop_callback`. It’s a powerful tool for unblocking threads, but it executes in the context of the thread that calls `request_stop()`. If your callback does anything heavy or locks a contested mutex, you’ve just turned your cancellation mechanism into a bottleneck.
  • Avoid the temptation to use `std::jthread` as a magic bullet. While it handles the stop source and joining for you, it doesn’t magically make your code thread-safe. It just automates the boilerplate that people usually mess up.
  • Check the token status frequently enough to matter, but not so often that you’re thrashing the cache. In a high-frequency loop, checking a `std::stop_token` is cheap, but if you’re doing it every single instruction, you’re just wasting cycles that could be spent on actual work.

The Bottom Line

Stop tokens are cooperative, not preemptive; if your worker thread doesn’t explicitly poll `stop_requested()`, the token is just an expensive piece of useless state.

Don’t try to build your own cancellation logic with raw atomics; `std::stop_source` handles the lifecycle and thread-safety of the signal so you don’t have to.

Always design your long-running loops around the token from the start, because retrofitting cancellation into a legacy execution path is a recipe for subtle, non-deterministic deadlocks.

The Cost of Ignoring the Signal

At the end of the day, stop tokens aren’t a magic wand that kills threads; they are a polite request for cooperation. If your worker loop is stuck in a heavy computation or a blocking syscall without checking `stop_requested()`, the token is just a useless piece of state sitting in memory. You have to integrate these checks into your inner-most loops and respect the handoff between the source and the token. I’ve seen too many engineers assume that because they have a `std::stop_source`, their threads are suddenly safe from hanging. They aren’t. The safety only exists if you actually honor the contract you’ve established between the signaling thread and the worker.

C++ gives you the primitives to build incredibly robust, responsive systems, but it doesn’t hold your hand through the implementation. It expects you to understand the mechanics of the lifecycle you are creating. Stop using “kill” signals and start using cooperative cancellation. It’s more work, and it requires a disciplined approach to your concurrency model, but it’s the difference between a system that shuts down gracefully and one that leaves behind a trail of zombie threads and corrupted state. Stop fighting the language and start writing code that respects the machine.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Using structured bindings in real code.

Structured Bindings Killed the Three Line Unpack

String operations that allocate memory in Python.

Every Plus Between Strings Is Another Allocation