The previous chapter covered what concurrency is and why anyone would want it. This chapter covers the concrete tool C++ gives you to do it, std::thread, the type introduced in C++11 that lets you start a new thread of execution and hand it a function to run. We'll cover how to construct one, how to pass arguments into it correctly, how to ask the runtime about thread identity and hardware, and how to make a thread pause without busy-waiting.
std::thread Actually Isstd::thread (since C++11, in <thread>) is a thin C++ wrapper around an OS-level thread. Constructing one starts a new thread immediately and runs whatever callable you hand it. The original thread (the one that did the construction) keeps running in parallel.
Two lines print, but the order isn't fixed. std::thread worker(refreshInventory); spawns a new thread that runs refreshInventory. While that thread runs, main keeps going and prints its own line. Whichever one the OS schedules first wins the race to std::cout. This is the simplest demonstration of concurrent execution you can write in C++.
The worker.join() line is there for one reason: the std::thread destructor calls std::terminate and kills your program if you destroy a thread object that's still running and you haven't told it what to do. The call appears here so the examples in this chapter don't crash.
Spawning an OS thread has overhead. On Linux it typically takes tens of microseconds and allocates a stack (default around 8 MB virtual memory per thread on x86-64). For tiny bits of work, the overhead is larger than the work itself. Thread pools (std::async, custom executors) exist for this reason.
std::thread takes any callable as its first argument. That gives four common shapes: a plain function, a lambda, a member function, and a functor (a class with operator()). They all do the same thing at runtime. Pick whichever is easiest to read at the call site.
The simplest form: pass the name of a function. The compiler implicitly converts the name to a function pointer.
Arguments listed after the callable are forwarded to it. std::thread emailer(sendOrderConfirmation, 1042) is roughly "start a new thread, then call sendOrderConfirmation(1042) inside it". The exact semantics around argument copying and references are tricky.
Lambdas (since C++11) are usually the most readable choice when the work is short or captures local state.
The lambda captures customer by value, so the new thread gets its own copy of the string. Capturing by reference works too, but only if the captured variable outlives the thread. That's a recurring theme in this chapter and the next.
To run a class's method on a new thread, pass a pointer-to-member-function and an object (or a pointer to one) for this.
The second argument, &resizer, becomes the this pointer inside resize. Anything after that is forwarded as the method's regular arguments. You can also pass the object itself instead of a pointer, and the thread will operate on a copy.
A functor is any object with operator() defined. You hand it to std::thread the same way you'd hand it a function.
There's a syntactic trap with functors. Constructing a thread from a temporary functor using parenthesized syntax produces something that looks like a thread but isn't.
What's wrong with this code?
That line is parsed as a function declaration: a function t that takes a parameter of type "pointer to a function taking nothing and returning LogWriter" and itself returns std::thread. The compiler doesn't see a constructor call. This is the famous "most vexing parse".
Fix (any of these works):
Brace initialization is the cleanest of the three and the one most modern code uses.
Arguments handed to std::thread are not passed straight through to the target function. The constructor stores its own copy of each argument inside the thread's internal storage and then calls the function with those copies. This changes the meaning of "pass by reference" in ways that are easy to get wrong.
Anything you pass is copied (or moved) into the thread's internal storage. Cheap for small types, potentially expensive for large ones.
The thread gets its own copy of name. The original variable in main is untouched.
Each argument is copied (or moved) twice in the worst case. Once into the thread's internal storage, then again when the storage is used to invoke the function. For large objects that don't need to be shared, prefer passing by value to a function that takes by value, or use std::move if the original isn't needed.
std::refIf the target function takes a parameter by reference, the argument still has to be wrapped in std::ref (or std::cref for const references) when handed to std::thread. Otherwise the thread copies the argument and the function reference binds to that copy, not the original.
What's wrong with this code?
Two outcomes are possible, depending on the compiler. With g++ and most modern standard libraries, this fails to compile with a long template error that boils down to "cannot bind a non-const reference to an rvalue". The internal storage holds a copy, and std::thread deliberately refuses to bind a non-const reference to that copy because the alternative behavior would be even worse. If the code somehow compiled, addToCart would modify the thread's private copy of items, not the items in main, and the output would be 0.
Fix:
std::ref(items) (in <functional>, since C++11) wraps items in a reference_wrapper that the thread stores and that decays back to a real reference when the function is called. The thread now operates on the actual items in main. Use std::cref if the parameter is const T&.
There's still a hazard here. The thread holds a reference to items. If items goes out of scope before the thread finishes touching it, the result is a dangling reference and undefined behavior. The join() call before returning keeps items alive long enough.
For move-only types like std::unique_ptr, the argument has to be moved explicitly with std::move. The same applies when avoiding the copy cost of a large object.
std::move(email) casts email to an rvalue reference, telling the thread "you can steal the guts of this". Inside the thread, the unique_ptr ends up owning the Email, and the original email in main is left empty.
Every running thread has a unique identifier of type std::thread::id. You can query the current thread's id with std::this_thread::get_id() and the id of any std::thread object with its get_id() member.
The exact format of the printed id is up to the standard library. libstdc++ prints a decimal number, libc++ prints a pointer-like hex value. Ids are comparable (==, !=, <), hashable (std::hash<std::thread::id>), and unique per running thread. A default-constructed std::thread::id represents "no thread" and compares equal only to other default-constructed ids.
Ids are useful in a few specific places: logging which thread did what, looking up per-thread state in a std::map<std::thread::id, ...>, or checking that a function is being called on the same thread that owns a resource. They are not useful for general identification of work; to tag work units, use an integer or name.
C++ exposes a hint from the runtime, std::thread::hardware_concurrency(). It returns the number of concurrent threads the system can run in parallel, usually the number of logical cores. The return type is unsigned int.
Two caveats apply. First, the result is a hint, not a guarantee. The standard explicitly allows it to return 0 if the implementation can't determine a useful answer. Production code should treat 0 as "unknown" and fall back to a sensible default like 4. Second, it reports how many threads the hardware can run in parallel, not how many threads to spawn. CPU-bound work is usually best served by hardware_concurrency() threads or one or two fewer. I/O-bound work (waiting on disk, network) can profitably use many more threads, since each one spends most of its time blocked.
Putting threads into a std::vector and joining them in a loop is the standard pattern for "fan out a fixed amount of work over N workers". The join() call here is the same one used earlier in this lesson.
Spawning many more threads than cores doesn't make CPU-bound code faster. It usually makes it slower because the OS scheduler has to context-switch between them, and context switches cost on the order of microseconds. For I/O-bound work, oversubscription often helps.
The diagram walks through the fan-out pattern. The main thread (cyan) asks the runtime how many cores it can use, then spawns that many worker threads (green), each of which the OS scheduler will try to place on its own core. Once all workers have started, main loops over them and waits for each to finish (teal) before continuing. This is the foundation of most CPU-bound parallel workloads in C++.
std::this_thread is a namespace (not a class) inside <thread> that holds utilities for the thread currently running. Beyond get_id(), the three common ones are yield, sleep_for, and sleep_until.
std::this_thread::yieldyield() tells the OS scheduler "I'm willing to give up the CPU right now, if someone else needs it more". The implementation isn't required to do anything, and on most systems the effect is: if there's another ready thread, switch to it; otherwise come right back.
The worker thread spins, checking a flag, but yields between checks so it doesn't pin a CPU at 100% busy-waiting. std::atomic is used here because two threads touch stockReady. This is a foreshadowing, not a full explanation.
Use yield when the thread has nothing useful to do but expects the situation to change soon. For "wait for X to happen", a condition variable (covered later) is almost always better. yield is mostly for low-level building blocks and spin loops.
yield is cheap (typically tens to hundreds of nanoseconds), but a tight loop calling yield still burns CPU. For waits longer than a few microseconds, use sleep_for or a condition variable instead.
std::this_thread::sleep_forsleep_for takes a std::chrono::duration and blocks the current thread for at least that long. The OS scheduler is free to wake you up later than requested, but never earlier.
The 200ms literal comes from the std::chrono_literals namespace. Without the using namespace, write std::chrono::milliseconds(200), which works equally well. sleep_for accepts any chrono duration: nanoseconds, microseconds, seconds, hours.
std::this_thread::sleep_untilsleep_until takes a std::chrono::time_point instead of a duration and blocks until that absolute moment. Use it for a deadline ("wake up at 9:00:00 AM") rather than a delay ("wake up 30 seconds from now").
steady_clock is monotonic; it never goes backwards. Use it for measuring time durations. system_clock is the wall-clock time and can jump if someone adjusts the system clock, so avoid it for "sleep for N seconds" logic.
The difference between sleep_for(d) and sleep_until(clock::now() + d) matters most inside a loop. If the loop body takes variable time and each iteration should start every 100 ms exactly, sleep_until lets the next wakeup be computed as start + i * 100ms. Using sleep_for(100ms) in the same loop would slowly drift, because each iteration's work time adds to the 100 ms wait.
The pieces are now in place to write something that runs in parallel. Three independent background tasks fire off, and main waits for all of them.
Three threads work at the same time. The total wall-clock time is roughly the longest single task, about 120 ms, not the sum of all three. That's the reason to use threads in the first place.
Two points apply. First, std::cout lines can interleave or get scrambled because multiple threads write to the same stream. Characters from two messages sometimes mix together. Output from threads is one of the first places race conditions appear. Second, every call to join() here is the same kind used earlier. Treat join as a black box that says "wait for this thread to finish".
Concurrent execution isn't a free speedup. Three 100 ms tasks that share no data run in roughly 100 ms total. Three 100 ms tasks that all hammer the same std::mutex can run in 300 ms or worse, because they're serialized again. Always check whether the work actually parallelizes before adding threads.
The reason for calling join() on every thread is worth stating directly. When a std::thread object's destructor runs, the standard says: if the thread is still joinable (still represents a running thread that hasn't been joined or detached), the destructor calls std::terminate. The program ends. There's no exception, no graceful shutdown, no recovery after the fact.
Compiling and running that program crashes immediately. The error message depends on the runtime, but it usually contains terminate called without an active exception. The thread object was destroyed with a live OS thread still inside it, and the C++ runtime had no choice but to kill the program.
That's why every example in this chapter calls join() before the thread variable goes out of scope. This chapter hasn't explained what join does or what the alternative (detach) means. The next chapter covers thread lifetime management properly, including join(), detach(), joinable(), and the safer std::jthread from C++20 that handles cleanup automatically.
10 quizzes