Free lesson · Concurrency Interview Mastery
Creating Threads in C++
C++ got real, standard threading in C++11 with std::thread, and the language's approach has a distinct personality: it gives you direct, low-level control and trusts you not to shoot yourself in the foot - which means C++ threading has a few sharp edges that will call std::terminate on your program if you ignore them. Coming from Java or Python, the mechanics feel familiar, but C++ demands more discipline about thread lifetime. This lesson covers creating threads and, just as importantly, the rules you must follow to avoid your program crashing on exit.
I'll be honest about my angle here: the code I work on at Google - Spanner - is a large C++ system where getting thread lifetime and resource management right isn't optional, it's the difference between a service that runs for years and one that crashes. So the discipline this lesson preaches isn't pedantry; it's the daily reality of writing C++ that has to survive in production.
Creating a thread with std::thread
You construct a std::thread with a callable - a function, a lambda, or any callable object - plus any arguments. Construction starts the thread immediately.
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Working on " << id << "\n";
}
int main() {
std::thread t(worker, 42); // starts running immediately on construction
t.join(); // wait for it to finish
return 0;
}
Unlike Java (where you create then start()), a std::thread begins executing the moment it's constructed - there's no separate start step. Lambdas are the idiomatic way to give a thread inline work:
int value = 10;
std::thread t([value]() {
std::cout << "Captured " << value << "\n";
});
t.join();
The rule that will crash your program: join or detach
This is the most important thing in the lesson, and it's unique to C++ among the languages in this course. Every std::thread must be either joined or detached before it is destroyed. If a std::thread object is destroyed while still joinable (still associated with a running thread that hasn't been joined or detached), the destructor calls std::terminate() - your whole program dies immediately.
{
std::thread t(worker, 1);
// forgot to join or detach...
} // t's destructor runs here -> std::terminate() -> program aborts
Your two options:
join()- block until the thread finishes, then the thread object becomes non-joinable. This is the common, safe choice: you wait for the work to complete.detach()- sever the thread from the object, letting it run independently in the background. The object becomes non-joinable, and the thread runs to completion on its own (or until the program exits).
flowchart TD
C["std::thread t(...) - joinable, running"] --> D{Before t is destroyed:}
D -->|join| J["join(): block until done, then non-joinable"]
D -->|detach| DE["detach(): run independently, non-joinable"]
D -->|neither| T["destructor -> std::terminate() - CRASH"]
This flowchart shows the lifetime rule for a C++ thread. When you construct a std::thread, it is joinable and running. Before that thread object is destroyed, you must do one of two things. Either call join, which blocks until the thread finishes and then makes the object non-joinable. Or call detach, which lets the thread run independently in the background and also makes the object non-joinable. If you do neither, when the thread object's destructor runs, it calls std::terminate, which crashes the entire program. So every std::thread must be joined or detached before destruction.
Warning: This is C++'s signature threading gotcha and a guaranteed interview question. Forgetting to join or detach doesn't give you a silent bug or a leaked thread like some languages - it terminates your program. The standard chose this harsh behavior deliberately: silently detaching would let threads outlive the data they reference (a dangling-reference disaster), and silently joining in a destructor could block unexpectedly. So the standard refuses to guess and crashes instead, forcing you to be explicit. The modern fix,
std::jthread, removes this footgun - covered below.
detach is more dangerous than it looks
detach() sounds convenient - fire and forget - but it's a sharp knife. A detached thread runs independently, which means it can easily outlive the variables it captured by reference, or still be running when main returns and the program tears down. Both are undefined behavior.
void risky() {
int local = 5;
std::thread t([&local]() { // captures local BY REFERENCE
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << local; // local is GONE by now - dangling reference, UB
});
t.detach();
} // risky() returns, 'local' destroyed, but the detached thread is still running
Caution: Prefer
join()overdetach()unless you have a specific reason. A detached thread is a thread you've lost control of - you can't wait for it, you can't know when it finishes, and it can easily reference data that's already been destroyed. Detached threads are a frequent source of crashes and use-after-free bugs. When you do detach, capture everything by value (or use shared ownership likeshared_ptr) so the thread doesn't depend on a stack frame that's about to vanish.
Passing arguments: copies by default
Arguments to a std::thread are copied into the thread's storage by default - a safety measure so the thread doesn't depend on the caller's variables. If you genuinely want to pass by reference, you must wrap with std::ref:
void increment(int& n) { n++; }
int x = 0;
std::thread t1(increment, x); // x is COPIED - original x unchanged
std::thread t2(increment, std::ref(x)); // x passed by reference - original x modified
Important: This copy-by-default behavior surprises people.
std::thread t(fn, x)copiesx, so a function takingint&won't see your original - you needstd::ref(x). Butstd::refreintroduces the lifetime danger: now the thread holds a reference, so that referenced object must outlive the thread. The copy default is the safe default; reach forstd::refdeliberately and make sure the referent lives long enough.
std::jthread: the modern, safe alternative (C++20)
C++20 added std::jthread, which fixes the terminate-on-destruction footgun. A jthread automatically joins in its destructor, so forgetting to join no longer crashes your program. It also supports cooperative cancellation via a stop_token.
#include <thread>
void worker(std::stop_token st, int id) {
while (!st.stop_requested()) {
// do work; check st periodically
}
}
int main() {
std::jthread t(worker, 42); // auto-joins when t goes out of scope
// ... t.request_stop() to signal cancellation ...
return 0;
} // t's destructor requests stop AND joins automatically - no terminate, no crash
Tip: Prefer
std::jthreadoverstd::threadin new C++20 code. It eliminates the most common C++ threading bug (forgetting to join) by joining automatically, and it adds clean cooperative cancellation viastop_token. The only reason to still usestd::threadis targeting pre-C++20 or needing its exact lower-level behavior. "I'd usestd::jthreadfor automatic joining and built-in stop tokens" is the current, savvy answer.
Useful helpers
A few things you'll use constantly:
std::this_thread::sleep_for(duration)- park the current thread for a duration.std::this_thread::yield()- hint to the scheduler to run another thread.std::this_thread::get_id()- the current thread's identifier, handy for logging.std::thread::hardware_concurrency()- the number of hardware threads (cores) available, useful for sizing thread pools.
Getting a return value from a thread, by the way, isn't done directly with std::thread - the callable's return value is discarded. For results, you use std::async/std::future, which is the final lesson in this C++ section.
The philosophy: explicit ownership and lifetime
It's worth pausing on why C++ threading feels stricter than Java's or Python's, because understanding the philosophy makes the rules feel principled rather than arbitrary. C++'s entire design ethos is that you, the programmer, are responsible for the lifetime of resources - and a running thread is a resource. The standard refuses to make lifetime decisions on your behalf, because any default it picked would be wrong some of the time. If it silently detached unjoined threads, those threads could outlive the data they reference, producing use-after-free crashes that are far worse than a clean abort. If it silently joined in the destructor, your program could block in surprising places, with a destructor unexpectedly waiting for a long-running thread. Faced with two bad defaults, the standard chose to refuse to guess and crash loudly instead, forcing you to state your intent.
This is the same principle that runs through all of C++: resources have owners, owners have well-defined lifetimes, and the language gives you sharp tools plus the responsibility to wield them correctly. A std::thread is an owning handle to an OS thread, and like any owning handle in C++, you must decide what happens to the owned resource before the handle dies. That decision is join (wait for it) or detach (let it go). There's no third option, and there's no safe default, so the language makes you choose. Once you see threads as just another owned resource, the join-or-detach rule stops feeling like a gotcha and starts feeling like the natural consequence of C++'s ownership model - the same model that, done right, is what lets large C++ systems manage millions of resources without a garbage collector.
This is also why std::jthread is such a clean improvement. It applies RAII - the idea that an object's destructor cleans up its resource automatically - to thread ownership: when a jthread is destroyed, it joins automatically, exactly the way a smart pointer frees its memory or a lock guard releases its mutex. jthread brings threads into the same automatic-lifetime discipline that governs the rest of well-written modern C++, which is why it's the right default in new code.
Common mistakes and how to avoid them
A handful of C++ threading mistakes account for the majority of real bugs, and naming them is good interview preparation as well as good engineering. The first, already covered, is forgetting to join or detach - which crashes via std::terminate. The defense is to use std::jthread so joining is automatic, or to be religious about joining every std::thread before its scope ends.
The second is capturing local variables by reference in a thread that may outlive them. A lambda that captures by reference ([&]) and is run on a detached thread, or even a joined thread whose referenced data is destroyed early, reads freed memory - undefined behavior. The defense is to capture by value ([=]) for anything the thread needs to keep, or to use shared ownership via std::shared_ptr so the data lives as long as any thread references it. When you must capture by reference, you take on the obligation to prove the referent outlives the thread, and that proof is often harder than it looks once detachment or exceptions enter the picture.
The third is letting an exception escape a thread's top-level function. If the callable a thread runs throws an exception that isn't caught within that thread, the program calls std::terminate - exceptions cannot propagate across thread boundaries. The defense is to wrap each thread's top-level function body in a try/catch that handles or logs exceptions locally, or to use std::async (covered later), which captures exceptions in the returned future so you can handle them where you collect the result. This asymmetry - that exceptions stay within their thread and crash if uncaught - surprises people coming from single-threaded C++ where exceptions propagate naturally up the stack.
The fourth is oversubscription: creating far more threads than the hardware can run, so the machine spends its time context-switching rather than computing. The defense is to size thread usage against std::thread::hardware_concurrency() for CPU-bound work, and to use a thread pool or std::async rather than spawning a fresh thread per task. As in every language, raw thread-per-task is rarely the right pattern at scale; you pool and reuse.
When to reach for raw threads versus higher-level tools
A practical question worth answering directly: given the sharp edges of std::thread, when should you actually use it versus the higher-level alternatives? The honest answer is that raw std::thread is the right tool less often than beginners assume. It's the correct choice when you need a specific, long-lived thread that you control directly - a dedicated background thread for a particular job, a thread whose lifetime you manage explicitly as part of an object's lifecycle, or low-level systems code where you need precise control over thread creation and scheduling. In those cases the directness of std::thread (or better, std::jthread) is exactly what you want.
But for the far more common pattern of "I have a batch of independent tasks and I want them run concurrently and to collect results," raw threads are the wrong tool. There you want std::async (covered in the final C++ lesson), which runs a task and hands you a std::future for its result and exceptions, or a thread pool that reuses a fixed set of threads across many tasks. These higher-level tools handle result retrieval, exception propagation, and thread reuse - exactly the things that are awkward and error-prone with raw std::thread, where return values are discarded and exceptions crash the program. The general principle, true in every language in this course, is that thread-per-task with raw threads doesn't scale and reinvents machinery the library already provides correctly.
So the mental hierarchy is: prefer the highest-level tool that fits. For "run a task, get a result," reach for std::async. For many tasks over a pool, reach for a thread pool. Drop down to raw std::jthread when you genuinely need a controllable long-lived thread, and to raw std::thread only when you're targeting pre-C++20 or need its exact low-level semantics. Choosing the right level - rather than reaching for raw threads reflexively because they're the first thing you learned - is part of writing C++ concurrency that's both correct and maintainable, and it's the kind of judgment an interviewer is listening for when they ask how you'd structure concurrent work.
The interview angle
Interview: Reliable C++ threading questions: (1) "What happens if you don't join or detach a
std::thread?" - Its destructor callsstd::terminate()and the program aborts; you must explicitly join or detach every thread before it's destroyed. (2) "join()vsdetach()?" -joinblocks until the thread finishes;detachlets it run independently and lose its handle. Preferjoin;detachrisks the thread outliving referenced data. (3) "How are arguments passed?" - By copy; usestd::reffor references (and ensure the referent outlives the thread). (4) "What'sstd::jthread?" - A C++20 thread that auto-joins on destruction and supportsstop_tokencancellation, fixing the terminate footgun. Nailing question (1) especially signals you've actually written C++ threads, because everyone who has, has hit it.
Quick reference
#include <thread>
std::thread t(fn, arg); // starts immediately; args COPIED
t.join(); // wait (preferred) OR t.detach();
// MUST join or detach before destruction, or std::terminate()!
std::thread t(fn, std::ref(x)); // pass by reference (ensure x outlives t)
std::jthread jt(fn, arg); // C++20: auto-joins, supports stop_token
std::this_thread::sleep_for(std::chrono::milliseconds(100));
unsigned n = std::thread::hardware_concurrency(); // core count hint
// return values: use std::async/std::future (later lesson)
Next we go beneath the threads to C++'s memory model - and here C++ is harsher than any language we've seen, because in C++ a data race isn't just a wrong answer, it's undefined behavior, which can mean anything at all.