Concurrency[conc]

std::thread and std::jthread: start, join and stop a thread

Section
Concurrency [conc]
Standard
C++20
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
5 min read
In this answer10 clauses
  1. 1Start a thread with std::jthread
  2. 2Forget to join a std::thread and the program ends
  3. 3Pass arguments: copies, std::ref and lambda captures
  4. 4Get a result back from a thread
  5. 5An exception that leaves a thread ends the program
  6. 6Stop a thread: std::stop_token and request_stop()
  7. 7join, detach and joinable
  8. 8Sleep, thread ids and hardware_concurrency
  9. 9What you need to build it
  10. 10std::thread or std::jthread: which one to write

Abstract

Start a thread in C++20 by constructing a std::jthread from a function and its arguments; its destructor requests a stop and joins the thread. Arguments are copied in the calling thread, so pass a reference with std::ref. A std::thread (C++11) must be joined on every path: destroying one that is still joinable calls std::terminate, and so does an exception that leaves the thread’s function, with either class.

A horizontal line forks at a dot into a second line that curves down and runs below it. Both carry hatched blocks of work. The lower line curves back up and rejoins the first at a purple dot.
Figure 1 — A thread is started, runs beside its caller, and is joined.

To start a thread in C++20, construct a std::jthread from a function and its arguments: std::jthread worker(drain, std::ref(queue));. The function runs on a new thread, its arguments are copied in the calling thread first (std::ref passes a reference), and when worker goes out of scope its destructor asks the thread to stop and joins it. std::thread (C++11) starts a thread the same way but must be joined by hand: destroying one that is still joinable calls std::terminate. So does an exception that leaves the thread’s function, with either class.

This answer is written for C++20, which added std::jthread, std::stop_token and std::stop_callback (P0660R10) to the C++11 std::thread. C++23 reworded how the constructor copies its arguments and added a std::formatter for std::thread::id; thread attributes such as std::thread::name_hint came after C++26, in the working draft. cppreference’s std::jthread page lists every member. Since When’s C++20 table gives the first releases of std::jthread, and its C++11 table the support for std::thread.

The samples were built with four compilers on three standard libraries: MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64). Each output shown is what all four printed, unless the text says otherwise. The std::thread samples are C++11 code, built as C++14 on all four (MSVC has no C++11 mode) and also as C++11 with GCC and libc++.

The Linux builds of the samples that end normally also ran clean under ThreadSanitizer, one of the sanitizers on the Toolchain page.

What changed: a thread is started with std::thread, and join() on every path std::jthread, which joins itself.

since C++20

Start a thread with std::jthread

Any callable starts a thread: a function and its arguments, a lambda or another of the function objects C++ has, or a member function followed by a pointer to its object. The running example processes jobs, each an id and a size in bytes:

[Example 1:
struct Job {
    int id;
    std::size_t size;
};

void process(const Job& job) {
    std::cout << "job " << job.id << ": " << job.size << " bytes\n";
}
// ...
{
    std::jthread worker(process, Job{1, 4096});  // started
}   // ~jthread(): waits for the thread here
std::cout << "main: job 1 is done\n";
// ...
{
    Job job{2, 512};
    std::jthread worker([job] { process(job); });
}
// ...
Worker worker(std::vector<Job>{{3, 128}, {4, 64}});
{
    std::jthread t(&Worker::run, &worker);
}
// ...
std::thread t(process, Job{5, 1024});
t.join();                       // std::thread: join by hand
std::cout << "main: joined\n";

— end example]

Worker::run() calls process() on each of its own jobs. It printed:

[Example 2:
job 1: 4096 bytes
main: job 1 is done
job 2: 512 bytes
job 3: 128 bytes
job 4: 64 bytes
job 5: 1024 bytes
main: joined

— end example]

~jthread() calls request_stop(), then join() ([thread.jthread.class]); std::thread takes the same arguments and leaves the join() to you.

Forget to join a std::thread and the program ends

Destroying a std::thread that is still joinable calls std::terminate ([thread.thread.destr]). The shortest way there:

[Example 3:
int main() {
    std::thread worker(process, Job{1, 4096});
    std::cout << "leaving main" << std::endl;
}   // ~thread(): still joinable, so std::terminate()

— end example]

All four printed leaving main; what came after is each runtime’s own:

Runtime Message Exit status
Microsoft’s STL, release build (MSVC, Clang 19.1) nothing 0xC0000409
Microsoft’s STL, debug build (MSVC, /MDd) abort() has been called, in a dialog box 3
libstdc++ (GCC 16.2) terminate called without an active exception 134, SIGABRT
libc++ (Clang 23.1) libc++abi: terminating 134, SIGABRT

No tool caught it: no warning at /W4 or -Wall -Wextra, nothing from MSVC’s /analyze, and style findings only from clang-tidy 19.1.5 with every check on.

The usual way there: the join() is written, but an exception or an early return skips it:

[Example 4:
std::size_t total_bytes(const std::vector<Job>& jobs) {
    std::size_t bytes = 0;
    std::thread worker([&] {
        for (const Job& job : jobs)
            bytes += job.size;
    });
    check(jobs);        // throws: worker.join() is never reached
    worker.join();
    return bytes;
}

— end example]

check() throws for an empty job, and the caller’s catch (const std::exception&) never ran: unwinding destroyed the joinable worker first, and the release and Linux builds ended as above. libstdc++ printed terminate called without an active exception here too: to it an exception is active only once a catch has it ([except.handle]), and this one was still on its way to the caller’s handler. The message points away from the cause.

As a std::jthread, worker joins on the way out and the exception reaches the caller: all four printed caught: job 2 is empty.

Property std::thread std::jthread
Standard C++11 C++20
Destroyed while joinable calls std::terminate request_stop(), then join()
Move-assigned over while joinable calls std::terminate request_stop(), then join()
Can be asked to stop no yes, request_stop()
Copy and move move-only move-only
Size, Microsoft’s STL on x64 16 bytes 24 bytes
Size, libstdc++ and libc++ on x86-64 8 bytes 16 bytes

Pass arguments: copies, std::ref and lambda captures

The constructor copies (or moves) every argument in the calling thread, and the function receives those copies ([thread.thread.constr]). A const& parameter binds to the thread’s copy unless the argument is wrapped in std::cref:

[Example 5:
void tally(const std::vector<Job>& jobs, std::size_t& bytes) {
    for (const Job& job : jobs)
        bytes += job.size;
}
// ...
std::thread first(tally, jobs, std::ref(bytes));  // jobs: a copy
jobs.push_back({3, 128});    // first counts its own copy
first.join();
std::cout << "first: " << bytes << " bytes\n";
// ...
bytes = 0;
std::thread second(tally, std::cref(jobs), std::ref(bytes));
second.join();               // jobs stays untouched until here
std::cout << "second: " << bytes << " bytes\n";

— end example]

It printed first: 4608 bytes and second: 4736 bytes. A non-const reference parameter without std::ref does not compile:

[Example 6:
std::thread worker(tally, jobs, bytes);  // error: needs std::ref

— end example]

No compiler’s message names std::ref. GCC’s comes closest:

[Example 7:
error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues

— end example]

libc++ fails a static_assert with an empty message; MSVC reports error C2672: 'invoke': no matching overloaded function found.

Counted with a Batch type whose constructors record the thread they run on, all three libraries did the same:

Parameter Argument Copies and moves
Batch batch 1 copy in the calling thread, 1 move in the new thread
const Batch& batch 1 copy in the calling thread
const Batch& std::move(batch) 1 move in the calling thread

So a copy constructor that throws does so in the calling thread: the constructor throws, no thread starts, and the caller’s catch gets it (all four, in every mode built). A move-only argument goes in with std::move: std::thread worker(process, std::move(job)); handed a std::unique_ptr<Job> to the thread and left main’s pointer empty.

A lambda that captures by reference shares the caller’s objects instead, and their lifetime is then yours (CP.31: pass small amounts of data by value). A jthread joins in its destructor, and locals are destroyed in reverse order of declaration, so what the thread uses comes first:

[Example 8:
struct Totals {
    std::size_t bytes = 0;
    ~Totals() { std::cout << "totals destroyed\n"; }
};

void run(const std::vector<Job>& jobs) {
    Totals totals;                  // declared first: destroyed last
    std::jthread worker([&] {
        for (const Job& job : jobs)
            totals.bytes += job.size;
        std::cout << "worker done: " << totals.bytes << " bytes\n";
    });
}   // ~jthread() joins the worker, then ~Totals() runs

— end example]

All four printed worker done: 4736 bytes before totals destroyed. Here the compiler enforces the order: declared after worker, totals cannot be captured at all, and all four rejected it.

A class is where it bites. Members are constructed in declaration order and destroyed in reverse ([class.base.init], [class.dtor]), so a std::jthread member declared before the members its thread uses starts before they are constructed and joins after they are destroyed. A thread that touches them in either window has undefined behavior:

[Example 9:
class Pipeline {
// ...
private:
    std::jthread worker;   // declared first: destroyed last
    Totals totals;         // declared last: destroyed first
};

— end example]

This thread never touches totals: it waits for its stop, then prints. All four printed totals destroyed before worker: stop requested. Declare the jthread member last.

Get a result back from a thread

The function’s return value is thrown away. Write the result into an object the caller owns, one slot per thread, and read it after the join: the end of the thread synchronizes with the return of join() ([thread.thread.member]), which is what makes the read safe. total() adds up the sizes in a span of jobs:

[Example 10:
std::span<const Job> all(jobs);
std::size_t half = all.size() / 2;
std::array<std::size_t, 2> bytes{};    // one slot per thread
{
    std::jthread a([&] { bytes[0] = total(all.first(half)); });
    std::jthread b([&] { bytes[1] = total(all.subspan(half)); });
}   // both joined: now the slots can be read
std::cout << bytes[0] << " + " << bytes[1] << " = "
          << bytes[0] + bytes[1] << " bytes\n";

— end example]

It printed 4608 + 2176 = 6784 bytes. Two threads writing one object, or a read of a slot before its thread is joined, with no mutex, atomic or join to order them, is a data race, and a data race is undefined behavior ([intro.races]). Share data through a std::mutex or a std::atomic; for one result from one task, std::async hands it back in a future.

For any number of threads, fill a std::vector<std::jthread> with emplace_back; its destructor joins every one:

[Example 11:
std::vector<std::size_t> sizes{4096, 512, 128, 2048};
std::vector<std::size_t> doubled(sizes.size());   // one slot per thread
{
    std::vector<std::jthread> workers;
    for (std::size_t i = 0; i < sizes.size(); ++i)
        workers.emplace_back([&, i] { doubled[i] = sizes[i] * 2; });
}   // ~vector(): every ~jthread() joins

— end example]

All four printed the sum, 13568 bytes.

A jthread does not make the result readable at return: the return expression is evaluated before the locals’ destructors run. This worker loops until its stop is requested:

[Example 12:
std::size_t read(const std::size_t& bytes) {
    std::cout << "return: bytes read\n";
    return bytes;
}

std::size_t total_bytes() {
    std::size_t bytes = 0;
    std::jthread worker([](std::stop_token token) {
        while (!token.stop_requested())
            std::this_thread::sleep_for(std::chrono::milliseconds(5));
        std::cout << "worker: stopped by ~jthread()\n";
    });
    return read(bytes);   // evaluated before ~jthread() runs
}

— end example]

All four printed return: bytes read before worker: stopped by ~jthread(). A function that returns what its worker wrote calls worker.join() before the return, jthread or not.

An exception that leaves a thread ends the program

An exception that leaves the thread’s function calls std::terminate ([thread.thread.constr]), with std::jthread too: the same end as an exception thrown out of a noexcept destructor, which a destructor is by default since C++11. A try around the constructor cannot catch it, because it is thrown on the other thread:

[Example 13:
void process(const Job& job) {
    if (job.size == 0)
        throw std::invalid_argument("job " + std::to_string(job.id)
                                    + " is empty");
}

int main() {
    std::cout << "starting job 2" << std::endl;
    try {
        std::jthread worker(process, Job{2, 0});
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n';  // never printed
    }
}

— end example]

All four printed starting job 2. libstdc++ then printed:

[Example 14:
terminate called after throwing an instance of 'std::invalid_argument'
  what():  job 2 is empty

— end example]

libc++ printed libc++abi: terminating due to uncaught exception of type std::invalid_argument: job 2 is empty; both ended with SIGABRT. Microsoft’s release runtime again printed nothing and exited with 0xC0000409.

Catch inside the function, and hand the exception to the caller as a std::exception_ptr, rethrown after the join:

[Example 15:
std::exception_ptr error;           // written by the worker only
{
    std::jthread worker([&error] {
        try {
            process(Job{2, 0});
        } catch (...) {
            error = std::current_exception();
        }
    });
}   // joined: now error can be read
try {
    if (error)
        std::rethrow_exception(error);
} catch (const std::exception& e) {
    std::cout << "caught in main: " << e.what() << '\n';
}

— end example]

All four printed caught in main: job 2 is empty.

Stop a thread: std::stop_token and request_stop()

The standard library has no call that kills a thread (that would be a platform call on native_handle()); stopping is cooperative. A jthread passes its own std::stop_token to a function whose first parameter takes one; request_stop(), or the destructor, sets it:

[Example 16:
void serve(std::stop_token token, int id) {
    using namespace std::chrono_literals;
    while (!token.stop_requested())
        std::this_thread::sleep_for(5ms);   // look for work
    std::cout << "worker " << id << ": stop requested\n";
}
// ...
std::jthread first(serve, 1);   // runs serve(token, 1)
first.request_stop();
first.join();
std::cout << "main: worker 1 joined\n";
// ...
{
    std::jthread second(serve, 2);
}   // ~jthread(): request_stop(), then join()
std::cout << "main: worker 2 joined\n";

— end example]

Timeline: main forks at std::jthread worker(…) into a worker line of hatched loop passes. A purple request_stop() arrow drops to the worker, which then ends; a join() arrow goes back up to main.
Figure 2 — When a std::jthread still owns a thread, its destructor asks the thread to stop, then waits for it.

A member function that takes a std::stop_token does not receive it from std::jthread t(&Worker::run, &worker);: the token would have to come before the object, and all four rejected the call. A lambda that takes the token and passes it on worked on all four: std::jthread t([&worker](std::stop_token token) { worker.run(token); });.

A worker asleep in std::condition_variable_any::wait(lock, token, pred) is woken by the stop request: the call returns at once with the value of pred() ([thread.condvarany.intwait]):

[Example 17:
std::mutex mutex;
std::condition_variable_any job_added;
std::vector<Job> queue;          // guarded by mutex
std::size_t bytes = 0;           // the worker's; read after join

std::jthread worker([&](std::stop_token token) {
    std::unique_lock lock(mutex);
    while (job_added.wait(lock, token,
                          [&] { return !queue.empty(); })) {
        bytes += queue.back().size;
        queue.pop_back();
    }
    // wait() returned false: a stop was requested
    std::cout << "worker: stopped, " << queue.size()
              << " jobs waiting\n";
});
// ...
worker.request_stop();           // wakes the wait
worker.join();
std::cout << "main: " << bytes << " bytes processed\n";

— end example]

With one 4096-byte job queued before the stop, all four printed worker: stopped, 0 jobs waiting. Plain std::condition_variable has no such overload: all four rejected the call.

To run code at the moment of the stop, register a std::stop_callback on the token. Registered before the stop, it runs inside request_stop(), on the requesting thread; registered after, it runs at once, in its own constructor. Here serve takes the token alone:

[Example 18:
const std::thread::id main_id = std::this_thread::get_id();
std::jthread worker(serve);

std::stop_callback on_stop(worker.get_stop_token(), [&] {
    bool here = std::this_thread::get_id() == main_id;
    std::cout << "on_stop runs in main's thread: "
              << (here ? "yes" : "no") << '\n';
});
std::cout << "requesting a stop\n";
worker.request_stop();      // runs on_stop before it returns
std::cout << "request_stop() returned\n";

std::cout << "registering a callback after the stop\n";
std::stop_callback late(worker.get_stop_token(), [] {
    std::cout << "late: runs at once, in its constructor\n";
});
std::cout << "late is constructed\n";

worker.join();
std::cout << "main: joined\n";

— end example]

It printed:

[Example 19:
requesting a stop
on_stop runs in main's thread: yes
request_stop() returned
registering a callback after the stop
late: runs at once, in its constructor
late is constructed
main: joined

— end example]

join, detach and joinable

joinable() returns get_id() != std::thread::id(): true while the object owns a thread it has not joined or detached ([thread.thread.member]). All four agreed in every state:

State joinable()
Default-constructed false
Started true
After join() false
After detach() false
Moved from false
Moved to true

A second join() throws std::system_error with errc::invalid_argument on all four. Only what() differs:

  • Microsoft’s STL: invalid argument: invalid argument
  • libstdc++: Invalid argument
  • libc++: thread::join failed: Invalid argument

join() has no timeout. To wait at most so long, request a stop, or wait with wait_for on a condition variable or a future.

After detach() the object no longer represents the thread, and no join() can wait for it: only a signal the thread sends itself, through a condition variable or a future, tells you it ended. Hence the Core Guidelines’ CP.26, “Don’t detach() a thread”: a detached thread is hard to monitor and to communicate with.

When main returns, std::exit destroys the static objects while a detached thread may still run; if the thread then uses one of them, or most of the standard library (std::cout included), the behavior is undefined ([basic.start.term]). The standard does not say whether the thread is stopped.

Sleep, thread ids and hardware_concurrency

A thread acts on itself through std::this_thread, and on the thread an object owns through its members:

Call What it does
std::this_thread::sleep_for(50ms) blocks the calling thread for a duration
std::this_thread::sleep_until(t) blocks it until the time point t
std::this_thread::yield() offers the implementation a chance to reschedule
std::this_thread::get_id() the calling thread’s id
worker.get_id() the id of the thread worker owns, or std::thread::id()
std::thread::hardware_concurrency() a hint; should be 0 when it cannot be computed
worker.native_handle() the platform’s handle, for priority, affinity or a name

An id is unique among threads that are running or can still be joined; the library may reuse the id of a thread that has ended and can no longer be joined ([thread.thread.id]). Sleeping is not a way to wait for another thread: join() waits for it to end, and a condition variable waits for it to signal.

What you need to build it

std::thread and std::jthread are in <thread>, the stop tokens in <stop_token>. As C++17, all four rejected std::jthread; GCC adds note: 'std::jthread' is only available from C++20 onwards. As C++20, __cpp_lib_jthread was 201911 on all three libraries. The first releases with std::jthread:

Library First release
libstdc++ GCC 10.1
libc++ 20; from 18 behind -fexperimental-library
Microsoft’s STL Visual Studio 2019 16.9

Clang 23.1 with libc++ built every sample without -fexperimental-library. On Linux, no build passed -pthread, and all ran: Compiler Explorer’s glibc is 2.39, and since 2.34 libpthread is part of libc (the glibc 2.34 announcement: “New applications do not need to link with -lpthread”). On glibc 2.33 or older, g++ without the flag fails the link with undefined reference to 'pthread_create'.

std::thread or std::jthread: which one to write

You need Write
A worker thread in C++20 std::jthread, with a std::stop_token parameter if it loops
A thread in C++11 to C++17 code std::thread with join() on every path, or a joining wrapper such as gsl::joining_thread (CP.25)
One result from one task std::async and its future
Data that several threads write a std::mutex or a std::atomic
A thread that outlives the function that starts it a jthread owned by an outer scope, not detach() (CP.26)

Before the review:

  • Every std::thread is joined on every path, exceptions included, or is a std::jthread.
  • References go in through std::ref or std::cref; every other argument is copied first.
  • What a thread uses by reference is declared before the thread.
  • A result is read after join(), from a slot only that thread writes.
  • The thread’s function catches its own exceptions, and a std::exception_ptr carries one out.

More answers about threads and shared data are in Concurrency.

Questions

What is the difference between std::thread and std::jthread?

Two things. Destroying or move-assigning over a std::thread that is still joinable calls std::terminate, while a joinable std::jthread calls request_stop() and then join() in both cases. And a std::jthread owns a stop source: its function can take a std::stop_token as its first parameter and test stop_requested(). The rest of the interface (join(), detach(), joinable(), get_id()) is the same; std::thread is C++11, std::jthread C++20.

How do I pass a member function to std::thread?

Pass the member function pointer, then a pointer to the object, then the function’s own arguments: std::jthread t(&Worker::run, &worker); runs worker.run() on the new thread, and std::thread takes the same arguments. Only the pointer is copied, not the object, so the object must outlive the thread. A run() that takes a std::stop_token does not get the jthread’s token this way: start it with a lambda that takes the token and calls worker.run(token).

How many threads can I create, and what does hardware_concurrency return?

std::thread::hardware_concurrency() returns a hint, and should return 0 when the value is not computable or not well defined; it returned 2 in Compiler Explorer’s sandbox and another number on a Windows x64 machine. It is not a limit. When no more threads can be started, the std::thread or std::jthread constructor throws std::system_error with errc::resource_unavailable_try_again.

Do I need -pthread to use std::thread?

Not with glibc 2.34 or later, which moved libpthread into libc; its release announcement says new applications do not need to link with -lpthread. On a glibc 2.39 system, GCC 16.2 and Clang 23.1 with libc++ built and ran threads without -pthread, and a GCC -static build ran too. On glibc 2.33 or older, g++ without the flag fails the link with undefined reference to ‘pthread_create’.

Can I kill or cancel a std::thread?

No. Neither std::thread nor std::jthread has a call that ends another thread: stopping is cooperative. A std::jthread passes a std::stop_token to its function, request_stop() sets it, and the function returns when its loop sees stop_requested(). A thread blocked in std::condition_variable_any::wait(lock, token, pred) wakes when the stop is requested.

Is std::thread copyable?

No. std::thread and std::jthread are move-only: moving one hands over the thread it owns, and the moved-from object owns none, so its joinable() returns false. Move-assigning onto a std::thread that is still joinable calls std::terminate; a joinable std::jthread calls request_stop() and join() first.

Sources

  1. std::threadcppreference.com
  2. std::jthreadcppreference.com
  3. [thread.thread.constr] ConstructorsC++ standard working draft
  4. [thread.thread.destr] DestructorC++ standard working draft
  5. [thread.jthread.class] Class jthreadC++ standard working draft
  6. CP.25: Prefer gsl::joining_thread over std::threadC++ Core Guidelines
  7. CP.26: Don’t detach() a threadC++ Core Guidelines
  8. The GNU C Library version 2.34 is now availablelibc-alpha mailing list

Read next

  1. C++ move semantics: what std::move and && really doLanguage · Miriam Castellanos · written for C++20
  2. How to write comparators for std::sort and std::mapStandard Library · Miriam Castellanos · written for C++20
  3. C++ generators: co_yield and std::generatorConcurrency · Miriam Castellanos · written for C++23