std::jthread, introduced in C++20, is an upgrade to the original std::thread.
Its emergence mainly addresses two pain points of std::thread:
- No RAII: If
std::thread is still joinable (i.e., join() or detach() has not been called) when it is destructed, it will directly cause the program to crash (calls std::terminate). - Lack of thread stopping mechanism: Before C++20, stopping a thread midway usually required manual implementation.
Core Features of std::jthread
std::jthread follows the RAII principle. When a std::jthread object leaves its scope, its destructor automatically does two things:
- Calls
request_stop() (requests the thread to stop). - Calls
join() (waits for the thread to finish).
std::jthread supports passing a std::stop_token to the thread function. Inside the thread function, this token can be used to check if a stop request has been received.
Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <iostream>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
// The thread function receives stop_token as the first argument
void worker(std::stop_token st, int id) {
while (!st.stop_requested()) { // Check if stop is requested
std::cout << "Worker " << id << " is running...\n";
std::this_thread::sleep_for(1s);
}
std::cout << "Worker " << id << " received stop signal, exiting.\n";
}
int main() {
std::jthread t(worker, 1);
std::this_thread::sleep_for(3s);
// Explicitly request stop (optional, as the destructor will also call it automatically)
// t.request_stop();
std::cout << "Main thread leaving scope...\n";
// t leaves scope:
// 1. t.request_stop() is called
// 2. t.join() is called, main thread waits here for worker to finish
}
|
Using your own stop_source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| void worker(std::stop_token st, int id) {
while (!st.stop_requested()) {
std::cout << "Worker " << id << " is running...\n";
std::this_thread::sleep_for(200ms);
}
std::cout << "Worker " << id << " received stop signal, exiting.\n";
}
int main() {
// 1. Create an independent stop source
std::stop_source my_source;
// 2. Get token
std::stop_token my_token = my_source.get_token();
// 3. Start thread, passing your own token
// Note: Here we are not using the token automatically generated by jthread, but passing our own
std::jthread t(worker, my_token, 1);
std::this_thread::sleep_for(1s);
std::cout << "Main: Stopping via external source...\n";
// 4. Stop the thread via the external source
my_source.request_stop();
}
|
[!NOTE]
Example 1 uses jthread’s internal stop_source, so calling jthread.request_stop() stops it. Example 2 uses an externally passed stop_source; calling jthread.request_stop() will not successfully stop it because they are not the same stop_token. You need to use stop_source.request_stop().
Canceling multiple threads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| void worker(std::stop_token st, int id) {
while (!st.stop_requested()) {
std::cout << "Worker " << id << " is running...\n";
std::this_thread::sleep_for(200ms);
}
std::cout << "Worker " << id << " received stop signal, exiting.\n";
}
int main() {
std::stop_source my_source;
std::stop_token my_token = my_source.get_token();
std::jthread t1(worker, my_token, 1);
std::jthread t2(worker, my_token, 2);
std::this_thread::sleep_for(1s);
std::cout << "Main: Stopping via external source...\n";
// Stop all threads
my_source.request_stop();
}
|
Using stop_callback:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| int main() {
std::jthread t([](std::stop_token st) {
// Register a callback: once stop is requested, the callback executes immediately in the thread issuing the stop signal
std::stop_callback cb(st, []{
std::cout << "Callback: Stop requested!\n";
});
// Simulate a time-consuming operation
for (int i = 0; i < 10; ++i) {
if (st.stop_requested()) break;
std::cout << "Thread: Processing " << i << "...\n";
std::this_thread::sleep_for(200ms);
}
});
std::this_thread::sleep_for(600ms);
std::cout << "Main: Requesting stop!\n";
t.request_stop(); // This triggers the callback registered inside the child thread
std::cout << "Main thread: " << std::this_thread::get_id() << "\n";
// If stop was requested before registration, the callback is called immediately in the current thread of construction.
std::stop_callback callback_after_stop(t.get_stop_token(), [] {
std::cout << "Stop callback executed by thread: "
<< std::this_thread::get_id() << '\n';
});
}
|
Underlying Principles
The stopping mechanism of std::jthread is implemented based on a Shared State model. This mechanism consists of three core components:
std::stop_source: The issuer of the stop signal (Remote Control).std::stop_token: The receiver/observer of the stop signal (Traffic Light).std::stop_callback: The callback registrar when a stop signal is issued.
std::stop_source
- Role: Used to initiate a stop request.
- Principle: It holds a reference to the shared state. When
request_stop() is called, it triggers all registered callback functions. - In
jthread: std::jthread internally contains a std::stop_source member variable.
std::stop_token
- Role: Used to query the stop status.
- Principle: It also holds a reference to the same shared state, but it only has read permissions.
- API:
stop_requested(): Checks if the value in the shared state is true.stop_possible(): Checks if there is still an associated stop_source (if all sources are destroyed, stopping is impossible).
- In
jthread: When jthread starts a thread, it generates a stop_token from its internal stop_source (source.get_token()) and passes it to the thread function.
std::stop_callback
- Role: Callback executed when a stop signal is received.
- Principle: This is an RAII object. It registers a callback function for the associated
std::stop_token object. When the std::stop_source associated with the std::stop_token requests a stop, the callback function is invoked.
Architecture Diagram
You can imagine their relationship like std::shared_ptr. They all point to the same Control Block on the heap.
1
2
3
4
5
6
7
8
9
10
11
| [ std::jthread / std::stop_source ] [ std::stop_token ]
| |
| (Owns) | (Observes)
v v
+-------------------------------------------------------+
| Shared Stop State (Heap) |
|-------------------------------------------------------|
| 1. Atomic Bool: stop_requested (Is stop requested) |
| 2. Thread ID: requester_id (Who requested it) |
| 3. Callback List: Linked list of stop_callbacks |
+-------------------------------------------------------+
|
[!NOTE]
For understanding purposes only; actual implementation differs.
std::jthread Internal Implementation Logic
Simplified source code of std::jthread:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| class jthread {
private:
thread _thread; // Underlying thread
stop_source _ssource; // stop_source controlling the stop
public:
// Constructor
template <class _Fn, class... _Args>
requires (!is_same_v<remove_cvref_t<_Fn>, jthread>)
explicit jthread(_Fn&& func, _Args&&... args) {
// 1. Check if func can be called with stop_token as the first argument
if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>) {
// 2. Accepted, get token from source and pass it in
_thread._Start(_STD forward<_Fn>(func), _ssource.get_token(), _STD forward<_Args>(args)...);
} else {
// 3. Not accepted, start as a normal thread
_thread._Start(_STD forward<_Fn>(func), _STD forward<_Args>(args)...);
}
}
// Get internal stop_source
std::stop_source get_stop_source() noexcept {
return _ssource;
}
// Get corresponding token
std::stop_token get_stop_token() noexcept {
return _ssource.get_token();
}
// Request stop
bool request_stop() noexcept {
return _ssource.request_stop();
}
~jthread() {
if (_thread.joinable()) {
request_stop(); // A. Request stop first
_thread.join(); // B. Then wait for completion
}
}
// ... Move constructor, assignment, etc. ...
};
|
It is worth noting specifically that:
1
| if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>)
|
This checks whether the passed function _Fn can be called with the specified arguments.
That is:
1
2
3
4
5
6
7
| void worker(std::stop_token st, int x) { ... }
// Check if worker() signature is (std::stop_token, int)
bool r = std::is_invocable_v<decltype(worker), std::stop_token, int>; // true
// Signature without stop_token
void worker(int x) { ... }
bool r = std::is_invocable_v<decltype(worker), std::stop_token, int>; // false
|
Note decay_t<_Args>...>, if you pass a stop_token yourself, it expands to:
1
| std::is_invocable_v<decltype(worker), std::stop_token, std::stop_token, int>;
|
This does not match the worker(std::stop_token, int) signature, so it enters the else branch:
1
| _Impl._Start(_STD forward<_Fn>(func), _STD forward<_Args>(args)...);
|
stop_source
stop_source manages a shared stop state (_Stop_state), implementing resource sharing and lifecycle management via reference counting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| class stop_source {
public:
stop_source() : _State{new _Stop_state} {}
explicit stop_source(nostopstate_t) noexcept : _State{} {}
stop_source(const stop_source& _Other) noexcept : _State{_Other._State} {
const auto _Local = _State;
if (_Local != nullptr) {
// `fetch_add(2)` instead of 1
// Reason: The lowest bit of `_Stop_sources` is used for marking, actual count is `value/2`
_Local->_Stop_sources.fetch_add(2, memory_order_relaxed);
}
}
stop_source(stop_source&& _Other) noexcept : _State{ exchange(_Other._State, nullptr)} {}
stop_source& operator=(const stop_source& _Other) noexcept {
stop_source{_Other}.swap(*this);
return *this;
}
stop_source& operator=(stop_source&& _Other) noexcept {
stop_source{move(_Other)}.swap(*this);
return *this;
}
~stop_source() {
const auto _Local = _State;
if (_Local != nullptr) {
// 1. source count -1 (actually -2)
// Right shift 1 bit to ignore flag bit, 2>>1 == 1 means it is the last one
if ((_Local->_Stop_sources.fetch_sub(2, memory_order_acq_rel) >> 1) == 1) {
if (_Local->_Stop_tokens.fetch_sub(1, memory_order_acq_rel) == 1) {
// 2. Last source, try to release token count
delete _Local;
}
}
}
}
stop_token get_token() const noexcept {
const auto _Local = _State;
if (_Local != nullptr) {
_Local->_Stop_tokens.fetch_add(1, memory_order_relaxed);
}
return stop_token{_Local};
}
bool stop_requested() const noexcept {
const auto _Local = _State;
return _Local != nullptr && _Local->_Stop_requested();
}
bool stop_possible() const noexcept {
return _State != nullptr;
}
bool request_stop() noexcept {
const auto _Local = _State;
return _Local && _Local->_Request_stop();
}
private:
_Stop_state* _State; // Pointer to shared stop state
};
|
stop_token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| class stop_token {
friend stop_source;
friend _Stop_callback_base;
public:
stop_token() noexcept : _State{} {}
stop_token(const stop_token& _Other) noexcept : _State{_Other._State} {
const auto _Local = _State;
if (_Local != nullptr) {
_Local->_Stop_tokens.fetch_add(1, memory_order_relaxed);
}
}
stop_token(stop_token&& _Other) noexcept : _State{exchange(_Other._State, nullptr)} {}
stop_token& operator=(const stop_token& _Other) noexcept {
stop_token{_Other}.swap(*this);
return *this;
}
stop_token& operator=(stop_token&& _Other) noexcept {
stop_token{move(_Other)}.swap(*this);
return *this;
}
~stop_token() {
const auto _Local = _State;
if (_Local != nullptr) {
if (_Local->_Stop_tokens.fetch_sub(1, memory_order_acq_rel) == 1) {
delete _Local;
}
}
}
bool stop_requested() const noexcept {
const auto _Local = _State;
return _Local != nullptr && _Local->_Stop_requested();
}
bool stop_possible() const noexcept {
const auto _Local = _State;
return _Local != nullptr && _Local->_Stop_possible();
}
private:
explicit stop_token(_Stop_state* const _State_) : _State{_State_} {}
_Stop_state* _State;
};
|
stop_state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| struct _Stop_state {
atomic<uint32_t> _Stop_tokens = 1; // plus one shared by all stop_sources
atomic<uint32_t> _Stop_sources = 2; // plus the low order bit is the stop requested bit
_Locked_pointer<_Stop_callback_base> _Callbacks; // Stores all registered stop callbacks
// always uses relaxed operations; ordering provided by the _Callbacks lock
// (atomic just to get wait/notify support)
atomic<const _Stop_callback_base*> _Current_callback = nullptr; // Currently executing callback
_Thrd_id_t _Stopping_thread = 0; // Stopping thread ID
bool _Stop_requested() const noexcept {
return (_Stop_sources.load() & uint32_t{1}) != 0;
}
bool _Stop_possible() const noexcept {
return _Stop_sources.load() != 0;
}
bool _Request_stop() noexcept {
// Attempts to request stop and call callbacks, returns whether request was successful
// Atomically set stop bit, check if it's the first request
if ((_Stop_sources.fetch_or(uint32_t{1}) & uint32_t{1}) != 0) {
// another thread already requested
return false;
}
// Record stopping thread
_Stopping_thread = _Thrd_id();
// Execute all callbacks
for (;;) {
auto _Head = _Callbacks._Lock_and_load();
_Current_callback.store(_Head, memory_order_relaxed);
_Current_callback.notify_all();
if (_Head == nullptr) {
_Callbacks._Store_and_unlock(nullptr);
return true; // All callbacks executed
}
const auto _Next = _STD exchange(_Head->_Next, nullptr);
_STL_INTERNAL_CHECK(_Head->_Prev == nullptr);
if (_Next != nullptr) {
_Next->_Prev = nullptr;
}
_Callbacks._Store_and_unlock(_Next); // unlock before running _Head so other registrations
// can detach without blocking on the callback
_Head->_Fn(_Head); // might destroy *_Head
}
}
};
|
_Stop_state internally maintains two atomic counters and a linked list:
_Stop_sources: Source reference count (actual value / 2) (Bitwise trick, storing two pieces of information simultaneously)_Stop_tokens: Token reference count (actual value / 1)_Callbacks: Linked list of stop_callback
1
2
3
4
| atomic<uint32_t> _Stop_sources = 2;
// Binary: ...00000010
// 0th bit (LSB): Stop requested flag
// Bits 1-31: Actual source reference count
|
_Stop_sources uses a 32-bit integer to store two pieces of information simultaneously: whether a stop is requested, and the source reference count.
1
| atomic<uint32_t> _Stop_tokens = 1;
|
All stop_sources share a “virtual token”.
Conclusion
By exploring the source code of jthread, we can better grasp jthread, avoid pitfalls in daily development, and at the same time, the source code reflects the design philosophy of the standard library—separation of concerns—from which we can learn a lot.