Ready to practice?
Sign up to access interactive coding exercises and track your progress.
std::jthread (C++20)
Explore the improved thread class with automatic joining and cooperative cancellation.
std::jthread (C++20)
As we have seen, std::thread has a footgun: if you forget to call join() or detach(), your program crashes with std::terminate(). This is especially problematic with exceptions:
void dangerous() {
std::thread t(work);
might_throw(); // If this throws...
t.join(); // ...we never reach here
} // CRASH: t destroyed while joinable
We showed how to write RAII wrappers to handle this, but C++20 gives us a better solution built into the standard library: std::jthread.
Introducing std::jthread
std::jthread ("joining thread") is a std::thread that automatically joins in its destructor:
#include <iostream>
#include <thread>
void work() {
std::cout << "Working...\n";
}
int main() {
std::jthread t(work);
// No need to call join()!
} // t automatically joined here
If the std::jthread is still joinable when it is destroyed, it calls join() automatically. No more crashes from forgotten joins.
Exception Safety Built In
void now_safe() {
std::jthread t(work);
might_throw(); // Even if this throws...
// ...t's destructor will join
} // Exception propagates, but t joins cleanly first
Cooperative Cancellation with stop_token
std::jthread has another major feature: cooperative cancellation via std::stop_token. This provides a standardized way to ask a thread to stop.
The Old Way (Manual Flags)
Without stop_token, you would use an atomic flag:
#include <atomic>
std::atomic<bool> stop_requested{false};
void worker_old() {
while (!stop_requested) {
// Do work...
}
std::cout << "Stopped!\n";
}
int main() {
std::thread t(worker_old);
std::this_thread::sleep_for(std::chrono::seconds(1));
stop_requested = true; // Signal thread to stop
t.join();
}
This works, but you need to manage the flag yourself, and every worker function needs access to the right flag.
The New Way (stop_token)
With std::jthread, cancellation is built in:
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token stoken) {
while (!stoken.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); // stop_token passed automatically!
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // Ask thread to stop
// t.join() called automatically in destructor
}
Output:
Working...
Working...
Working...
Working...
Working...
Stop requested, cleaning up...
How stop_token Works
When you create a std::jthread, it creates an associated std::stop_source. If your thread function's first parameter is std::stop_token, the jthread automatically passes in a token connected to its stop source.
// The stop_token parameter is detected and filled automatically
void worker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
// ...
}
}
// Also works with additional parameters
void worker_with_args(std::stop_token stoken, int id, const std::string& name) {
while (!stoken.stop_requested()) {
std::cout << name << " " << id << " working\n";
}
}
int main() {
std::jthread t(worker_with_args, 42, "Alice");
// stop_token inserted automatically before 42 and "Alice"
}
Automatic Stop on Destruction
Here is the key feature: when a std::jthread is destroyed, it automatically requests a stop before joining:
void worker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "Stopping gracefully\n";
}
int main() {
{
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::milliseconds(350));
// No explicit request_stop() call
} // Destructor calls request_stop(), then join()
std::cout << "Thread finished\n";
}
Output:
Working...
Working...
Working...
Stopping gracefully
Thread finished
This means well-designed worker functions automatically clean up when their jthread goes out of scope.
The stop_token Family
C++20 provides three related types:
| Type | Purpose |
|---|---|
std::stop_token |
Check if stop was requested (read-only) |
std::stop_source |
Request a stop (write) |
std::stop_callback |
Execute callback when stop is requested |
stop_source
You can get the stop_source from a jthread to share stop control:
std::jthread t(worker);
std::stop_source& source = t.get_stop_source();
// Anyone with the source can request a stop
source.request_stop();
// Check if a stop has been requested
if (source.stop_requested()) {
std::cout << "Stop was requested\n";
}
stop_callback
You can register callbacks to run when a stop is requested:
void worker(std::stop_token stoken) {
std::stop_callback callback(stoken, []() {
std::cout << "Cleanup callback executed!\n";
});
while (!stoken.stop_requested()) {
// Do work...
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
#include <condition_variable>
std::mutex mtx;
std::condition_variable_any cv;
void waiter(std::stop_token stoken) {
std::unique_lock lock(mtx);
// Wait until stop is requested
// condition_variable_any works with stop_token!
cv.wait(lock, stoken, []() { return false; });
std::cout << "Woken up by stop request!\n";
}
Comparing std::thread and std::jthread
| Feature | std::thread | std::jthread |
|---|---|---|
| Auto-joins on destruction | No (terminates) | Yes |
| Built-in cancellation | No | Yes (stop_token) |
| Auto-requests stop on destruction | No | Yes |
| Available since | C++11 | C++20 |
When to Use Which
Use std::jthread when:
- You are on C++20 or later (preferred default)
- You want automatic joining
- You need cooperative cancellation
- Exception safety is important
Use std::thread when:
- You are stuck on C++11/14/17
- You specifically need
detach()behavior - You are integrating with existing code that uses
std::thread
A Complete Example
Here is a more realistic example showing a background worker that can be cleanly stopped:
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <optional>
class BackgroundProcessor {
std::queue<int> work_queue_;
std::mutex mutex_;
std::jthread worker_;
void process_work(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::optional<int> item;
{
std::lock_guard lock(mutex_);
if (!work_queue_.empty()) {
item = work_queue_.front();
work_queue_.pop();
}
}
if (item) {
std::cout << "Processing: " << *item << '\n';
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
std::cout << "Worker shutting down\n";
}
public:
BackgroundProcessor()
: worker_(&BackgroundProcessor::process_work, this) {}
// Destructor automatically stops and joins the worker!
void add_work(int item) {
std::lock_guard lock(mutex_);
work_queue_.push(item);
}
};
int main() {
{
BackgroundProcessor processor;
for (int i = 1; i <= 10; ++i) {
processor.add_work(i);
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Destroying processor...\n";
} // processor's destructor stops and joins worker
std::cout << "Done!\n";
}
Output:
Processing: 1
Processing: 2
Processing: 3
Processing: 4
Processing: 5
Destroying processor...
Worker shutting down
Done!
The worker thread cleanly shuts down when the BackgroundProcessor is destroyed, with no explicit shutdown code needed in main().
Summary
std::jthreadautomatically joins in its destructor - no more forgotten joinsstd::stop_tokenprovides cooperative cancellation built into the threadrequest_stop()asks a thread to stop;stop_requested()checks the requeststd::jthreaddestructor callsrequest_stop()thenjoin()automaticallystd::stop_callbacklets you run code when a stop is requested- Prefer
std::jthreadoverstd::threadin C++20 and later
std::jthread (C++20) - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!