CodeGym /Courses /C++ SELF /std::jthread and std::stop_token

std::jthread and std::stop_token

C++ SELF
Level 68, Lesson 3
Available

1. Why std::jthread is useful

When you write multithreaded code for the first time, it often looks innocent: “I will start a thread, it will do something, and I'll figure out the rest later.” The problem is that std::thread is very strict: if a thread object is destroyed while the thread is still running, the standard library prefers not to guess what you intended and to terminate the process via an abnormal scenario. From the language point of view that's more honest than “well, it sometimes works”.

std::jthread appeared as a more beginner-friendly option: it tries to make the safe scenario the default behavior. The idea is simple: if a thread is a resource, let's manage it like a resource with RAII (like we did with files, containers, smart pointers): create an object — resource acquired; leave scope — resource released.

std::thread vs std::jthread: RAII-style thread

If you look at std::thread and std::jthread as “manual” and “automatic” modes, then with std::thread you are responsible for remembering to turn off the light, close the door and feed the cat (i.e. call join() or deliberately detach()). With std::jthread the cat is still yours, of course, but the door will at least close itself: when a std::jthread object is destroyed the thread will be properly finished (via join()), and additionally a stop request will be sent (we'll talk about that with stop_token).

Let's put this into a table — it's easier to remember what to pick.

Property
std::thread
std::jthread
What happens if the object is destroyed while the thread is running abnormal process termination (a nasty surprise) proper waiting for completion (no surprise, but might “wait a long time”)
Do you need to call join() manually yes, almost always you can, but often you don't need to
Support for polite cancellation manual, with your own mechanisms built-in via stop_token
Copying not allowed (move-only) not allowed (move-only)
Main idea “you're an adult, you handle it” “let's make it safe by default”

A small technical (and slightly pedantic but useful) fact: even in the standard documents the subtleties of constructor argument requirements for thread/jthread are discussed (for example, that parameters must be move-constructible). This is a good indicator that the ownership and move model for threads is not an “implementation detail” but part of the design.

2. Starting std::jthread: safe teardown on scope exit

The first contact with std::jthread usually prompts the thought: “So it's almost std::thread.” And that's true: you still pass a function/lambda and arguments. But the key difference appears not at start time but at the moment of scope exit.

The shortest example: the thread lives as long as the variable


#include <iostream>
#include <thread>

int main() {
    {
        std::jthread t([] {
            std::cout << "worker: hi\n"; // worker: hi
        });
        std::cout << "main: inside scope\n"; // main: inside scope
    } // t is destroyed -> the thread is guaranteed to be finished

    std::cout << "main: after scope\n"; // main: after scope
}

Note: the order of the "worker" and "main" lines inside the scope may still vary (nondeterminism is still present). But after scope exit you definitely won't get “the thread is still running while we left”.

join() still exists and is sometimes useful

RAII doesn't forbid you from explicitly saying “wait for completion here”. Sometimes that's useful because it makes the code easier to read: you can see the exact point where we wait.

#include <iostream>
#include <thread>

int main() {
    std::jthread t([] {
        std::cout << "worker: done\n"; // worker: done
    });

    t.join(); // explicit synchronization point
    std::cout << "main: continue\n"; // main: continue
}

4. std::stop_token: cooperative thread cancellation

If the RAII part of std::jthread solves the “I forgot to finish the thread” problem, it doesn't solve another common problem: “my thread is actually infinite (or very long), and I need to be able to ask it to stop”.

The important word is “ask”. There is no safe, language-level “kill thread” button in standard C++. And that's not because the language designers “forgot”. Forced termination almost certainly breaks invariants: the thread may hold a resource, be mid-operation, writing data, and killing it would leave the program in a broken state.

std::stop_token introduces cooperative cancellation:

  • someone outside sends a stop request,
  • the thread inside periodically checks “have I been asked to finish?”,
  • and exits at a safe point.

Mini diagram of the stop protocol

flowchart TD
    A[main: create jthread] --> B[worker: work loop]
    B --> C{stop_requested?}
    C -- no --> B
    C -- yes --> D[worker: exit gracefully]
    A --> E["main: request_stop()"]
    E --> C

The point of the diagram is that stopping is a protocol, not a “magic command”.

5. How std::jthread passes a stop_token to the thread function

The most practical part: std::jthread can pass a std::stop_token to your thread function automatically if the callable accepts it.

There is an important detail that is easy for a beginner to miss: the token is passed only if the callable (function/lambda) has a parameter std::stop_token (usually as the first parameter). If there is no such parameter — jthread will start the function as usual, but “asking to stop” will be pointless: the thread simply doesn't check the request.

A thread that can stop

#include <iostream>
#include <stop_token>
#include <thread>

void worker(std::stop_token st) {
    while (!st.stop_requested()) {
        // simulate "work"
        std::cout << "tick\n"; // tick
        std::this_thread::sleep_for(std::chrono::milliseconds{50});
    }
    std::cout << "worker: stop observed\n"; // worker: stop observed
}

And usage:

#include <chrono>
#include <thread>

int main() {
    std::jthread t(worker);

    std::this_thread::sleep_for(std::chrono::milliseconds{130});
    t.request_stop(); // asked to stop

    // join is optional: RAII will finish everything on main exit
}

A thread that can't stop, and why that's dangerous

Imagine an infinite loop without checking the token. Then jthread on destruction will do the “protocol”: call request_stop() + join(), but join() will wait forever because the thread never exits. This is a very common trap: “I used jthread, so everything is safe.” No: jthread is safer, but not magical.

6. Practice: a background activity indicator in a console app

To avoid staying at the abstract “tick” level, let's weave the topic into a small, realistic part of a console application we developed earlier (a CLI utility with commands, logs, etc.). A common task: a command runs “for a long time” (network, file work, computations), and you want to show the user that the program is alive.

Important: we do not use delays as synchronization. sleep_for below is only a simulation of long work and a way to make examples observable.

A background spinner that can be stopped

#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>

void spinner(std::stop_token st) {
    const char frames[] = {'|', '/', '-', '\\'};
    std::size_t i = 0;

    while (!st.stop_requested()) {
        std::cout << "\rworking... " << frames[i % 4] << std::flush;
        ++i;
        std::this_thread::sleep_for(std::chrono::milliseconds{80});
    }

    std::cout << "\rworking... done\n"; // working... done
}

There are two small beginner-level “tricks” here. First, "\r" returns the caret to the start of the line, and we redraw the same line (to avoid spamming the console with many lines). Second, std::flush forces the output to appear immediately on the screen.

The “long operation” in our app

#include <chrono>
#include <thread>

void simulate_long_task() {
    std::this_thread::sleep_for(std::chrono::milliseconds{600});
}

Putting it together: run the spinner as a jthread, do the work, request stop

#include <thread>

int main() {
    std::jthread ui(spinner);

    simulate_long_task();

    ui.request_stop(); // asked the spinner to finish
}

Notice the beauty (and the subtle danger) of the solution. On one hand, the code is short and pleasant: the spinner lives “next to” the operation. On the other hand, this only works because the spinner is written correctly: it regularly checks stop_requested().

Why this is “controlled termination”

This lecture aims to fix the main meaning of jthread + stop_token: we make thread termination controlled along two axes.

The first axis is lifetime management via RAII. A std::jthread object lives in a specific scope, which becomes a visible boundary of background activity. This is useful not only for the computer but also for the human: you read the code and see “the background starts here and ends here”.

The second axis is stop control via a protocol. Instead of “the thread will die someday by itself” we have a clear contract: external code can call request_stop(), and internal code is obliged to periodically check the token and terminate at a safe point.

And it is important to state a limitation: stop_token only solves the stopping question. It doesn't solve “how to safely share data between threads”. If you start reading/writing a shared variable concurrently without rules — that's a different class of problems, and we deliberately do not dive into it here so the lecture doesn't turn into “a few words about mutex, a few words about atomics, and then everyone cries”.

7. Common mistakes when working with std::jthread and std::stop_token

Mistake #1: thinking that std::jthread will “stop” any thread by itself.
jthread does handle an important part of the work for you on scope exit, but it can't read your mind. If the thread function is an infinite loop without token checks, jthread will dutifully wait for its completion. At best you'll get “a hang on exit”, at worst you'll spend a long time hunting why the program doesn't terminate.

Mistake #2: calling request_stop() and expecting immediate termination.
request_stop() is not “killing the thread”. It's a request. The thread will observe it only when it reaches a stop_requested() check. If the check is once every 10 seconds, reaction to the stop will take up to 10 seconds. This is normal: you choose the check frequency and thus the trade-off between responsiveness and code simplicity.

Mistake #3: using sleep_for as “synchronization”.
Sometimes you want to do: “I'll sleep 100 ms and the thread will surely update the variable.” This is a very slippery slope: on another machine, under different load or scheduler phases everything can break. sleep_for is fine for simulating work and demos, but it doesn't define access rules to data and doesn't make the program correct.

Mistake #4: launching a thread that uses references to local variables without thinking about lifetime.
Even with jthread you can write dangerous code if the thread holds a reference to an object that will disappear before the thread finishes. In this lecture we've intentionally written examples so the thread either lives “in the same scope” or uses data that lives long enough. Once you go beyond those boundaries you must stop and honestly answer: “Who owns the data, and who guarantees it's still alive?”

Mistake #5: treating stop_token as a “universal multithreading tool”.
stop_token solves one problem: cooperative cancellation. It doesn't make console output non-interleaving, doesn't protect containers from concurrent access and doesn't replace architecture. If you keep that in mind, jthread + stop_token become an excellent, simple and safe tool for “background activity with lifetime boundaries”.

1
Task
C++ SELF, level 68, lesson 3
Locked
Automatic thread joining
Automatic thread joining
1
Task
C++ SELF, level 68, lesson 3
Locked
Cooperative counter
Cooperative counter
1
Task
C++ SELF, level 68, lesson 3
Locked
Working spinner
Working spinner
1
Task
C++ SELF, level 68, lesson 3
Locked
Early exit
Early exit
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION