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
This makes `std::jthread` the recommended choice for most threading scenarios in modern C++.

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));
    }
}
The callback runs in the thread that calls `request_stop()`, not the worker thread. This is useful for waking up blocked threads.
#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
`std::jthread` has a `detach()` method too, but it is less commonly needed due to the auto-join behavior.

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::jthread automatically joins in its destructor - no more forgotten joins
  • std::stop_token provides cooperative cancellation built into the thread
  • request_stop() asks a thread to stop; stop_requested() checks the request
  • std::jthread destructor calls request_stop() then join() automatically
  • std::stop_callback lets you run code when a stop is requested
  • Prefer std::jthread over std::thread in C++20 and later