Concurrency & Multithreading

C++11 gave the language its first standard, portable way to run code on multiple threads at once — this is where std::thread, mutexes, condition variables, and futures replace the platform-specific threading APIs C++ programmers used to reach for.

Before C++11, running two pieces of code truly at the same time meant reaching for a platform-specific API — pthreads on Linux and macOS, the Win32 threading API on Windows — with no portable way to write the same multithreaded program for both. C++11 closed that gap with a standard threading model built into the language and library: std::thread to launch work concurrently, std::mutex and its RAII wrappers to protect shared data from being read and written by two threads at once, condition variables to let threads wait efficiently for something to happen, and std::future/std::promise/std::async to get a result back from work running elsewhere. This page builds up all of it — creating and joining threads, why unsynchronized shared access is dangerous, the locking primitives that fix it, and the higher-level tools for passing results between threads — because modern C++ treats concurrency as a first-class part of the language, not an add-on.

Threads: Creating and Joining with std::thread

A std::thread object represents an independent unit of execution: give it a callable (a function, lambda, or function object) and it starts running that callable on a new operating-system thread immediately, concurrently with the thread that created it.

std::thread worker(greet, "engineer"); starts greet("engineer") running on a new thread the instant the object is constructed — the extra constructor arguments after the callable are forwarded to it, exactly like std::invoke. Main thread continues running... can print before or after Hello from thread — the two threads are genuinely racing, and the order between them is not guaranteed unless something explicitly synchronizes them. worker.join() blocks the calling thread until worker's function returns, which is what guarantees Worker has finished. prints only after the greeting.

#include <iostream>
#include <thread>

void greet(const std::string& name)
{
    std::cout << "Hello from thread, " << name << "!\n";
}

int main()
{
    std::thread worker(greet, "engineer");

    std::cout << "Main thread continues running...\n";

    worker.join(); // wait here until worker finishes

    std::cout << "Worker has finished.\n";
}

Detaching Threads

worker.detach() severs the std::thread object from the underlying OS thread, letting that thread continue running independently in the background — the caller no longer waits for it and no longer has any handle on it at all.

Once detached, t no longer represents a joinable thread — calling join() on it afterward is undefined, and the detached thread's lifetime is no longer tied to main() at all. If main() returns and the process exits before background_task finishes, that thread's remaining work is simply cut off; its std::cout output may never appear, and if it were touching data owned by main() (a local variable, say) that data may already be destroyed, which is undefined behavior.

#include <iostream>
#include <thread>
#include <chrono>

void background_task()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    std::cout << "background task finished\n"; // risky: may run after main() exits
}

int main()
{
    std::thread t(background_task);
    t.detach();

    std::cout << "main() continuing without waiting\n";
    // main() may return before background_task finishes its sleep and print
}

Passing Arguments to Threads Safely

By default, arguments passed to std::thread's constructor are copied (or moved) into internal storage for the new thread to use, even if the target function's parameter is a reference — this is a deliberate safety measure, since the calling thread's own copy of the argument could otherwise be destroyed or modified before the new thread gets to it.

increment expects an int&, but std::thread's constructor would still try to copy value into an int for internal storage and bind the reference to that copy, not to the caller's variable — a plain-copy semantics that silently defeats a by-reference function. std::ref(value) wraps value in a std::reference_wrapper<int>, which std::thread copies just fine (it is small and copyable) while still referring back to the original value, so increment genuinely modifies the caller's variable.

#include <iostream>
#include <thread>

void increment(int& counter)
{
    ++counter;
}

int main()
{
    int value = 0;

    // WITHOUT std::ref, 'value' would be copied, and incrementing the
    // copy inside the thread would never affect the caller's 'value'.
    std::thread t(increment, std::ref(value));
    t.join();

    std::cout << "value = " << value << "\n"; // 1
}

Data Races: Why Shared State Needs Protection

A data race happens when two or more threads access the same memory location at the same time, at least one of the accesses is a write, and there is no synchronization ordering the accesses relative to each other. A data race is undefined behavior in C++ — not just 'might get a surprising number,' but a specification-level guarantee that anything at all can happen.

++counter is not one atomic step — it reads counter, adds one, and writes the result back, as three separate operations. With four threads doing this 100,000 times each unsynchronized, two threads can both read the same value, both add one, and both write back the same incremented result, silently losing one of the increments. The final total is almost always less than 400000, and by an unpredictable amount that varies between runs.

#include <iostream>
#include <thread>
#include <vector>

int counter = 0;

void increment_many()
{
    for (int i = 0; i < 100000; ++i)
        ++counter; // DATA RACE: unsynchronized read-modify-write
}

int main()
{
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(increment_many);

    for (auto& t : threads)
        t.join();

    std::cout << counter << "\n"; // almost never exactly 400000
}

std::mutex, std::lock_guard, and std::unique_lock

A std::mutex (mutual exclusion) is a lock: only one thread can hold it at a time, and any thread that calls lock() while another thread holds it blocks until that thread calls unlock(). Manually pairing every lock() with an unlock() is fragile — an exception or an early return can skip the unlock() and leave the mutex locked forever — so idiomatic C++ almost always wraps a mutex in an RAII guard instead.

std::lock_guard<std::mutex> guard(counter_mutex); locks counter_mutex in its constructor and unlocks it in its destructor — the moment guard goes out of scope at the end of each loop iteration, the mutex is released automatically, even if an exception were thrown in between. Because only one thread can hold counter_mutex at a time, the read-modify-write of ++counter can never overlap between threads, and the final count is reliably 400000 every run.

std::unique_lock is a more flexible alternative to lock_guard: it can be constructed without immediately locking, unlocked and re-locked manually within its scope, and moved — all things lock_guard cannot do. It is what condition variables require, for exactly that flexibility.

unique_lock's extra flexibility comes at a small cost — it is slightly larger and slightly slower than lock_guard because it has to track whether it currently owns the lock. Use lock_guard by default for a simple, scope-bound critical section, and reach for unique_lock only when you need to unlock early, relock, transfer ownership, or hand the lock to a condition variable.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

int counter = 0;
std::mutex counter_mutex;

void increment_many()
{
    for (int i = 0; i < 100000; ++i)
    {
        std::lock_guard<std::mutex> guard(counter_mutex);
        ++counter; // now safe: only one thread at a time reaches this line
    }
}

int main()
{
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(increment_many);

    for (auto& t : threads)
        t.join();

    std::cout << counter << "\n"; // always exactly 400000
}
#include <mutex>

std::mutex m;

void example()
{
    std::unique_lock<std::mutex> lock(m); // locked here
    // ... critical section ...
    lock.unlock();                        // manually unlocked early
    // ... non-critical work, mutex is free for other threads ...
    lock.lock();                          // re-locked
    // ... more critical section ...
}   // unlocked again automatically here, since lock is still held

std::scoped_lock: Locking Multiple Mutexes Without Deadlock

Locking two mutexes one at a time creates a classic deadlock risk: if thread A locks mutex1 then tries to lock mutex2, while thread B locks mutex2 then tries to lock mutex1, both threads can end up waiting forever for a mutex the other one holds. C++17's std::scoped_lock (a variadic generalization of lock_guard) locks any number of mutexes together using a deadlock-avoidance algorithm, so this ordering problem cannot occur.

std::scoped_lock lock(mutex_a, mutex_b); locks both mutexes as a single atomic-feeling operation, internally using an algorithm equivalent to std::lock() that avoids the classic lock-ordering deadlock even if some other code elsewhere locks the same two mutexes in the opposite order. This is the modern, safer replacement for manually calling std::lock(mutex_a, mutex_b) followed by two separate lock_guards constructed with std::adopt_lock.

#include <mutex>

std::mutex mutex_a;
std::mutex mutex_b;

void transfer()
{
    std::scoped_lock lock(mutex_a, mutex_b); // locks both, deadlock-free
    // ... work requiring both mutexes ...
} // both unlocked automatically here

recursive_mutex and timed_mutex

An ordinary std::mutex deadlocks a thread against itself if that same thread tries to lock it twice without unlocking in between — even one thread cannot recursively re-enter a locked region. std::recursive_mutex allows exactly that: the same thread can lock it multiple times, as long as it unlocks it the same number of times.

Each recursive call locks rmutex again on the same thread that already holds it — with an ordinary std::mutex this would deadlock immediately, but recursive_mutex tracks which thread owns it and how many times, unlocking fully only once every lock() has a matching unlock(). Reaching for recursive_mutex is usually a sign the code's locking structure could be simplified, but it is the correct tool when genuine recursive re-entry into a locked region is unavoidable.

std::timed_mutex (and std::recursive_timed_mutex) add time-bounded locking attempts, so a thread can give up instead of blocking forever.

try_lock_for blocks for at most the given duration, returning true if it acquired the lock in time and false if it gave up — useful for code that would rather report a failure or try an alternative than block indefinitely on a contended mutex.

#include <mutex>

std::recursive_mutex rmutex;

void recursive_function(int depth)
{
    std::lock_guard<std::recursive_mutex> guard(rmutex);
    if (depth > 0)
        recursive_function(depth - 1); // safe: same thread relocking rmutex
}
#include <mutex>
#include <chrono>
#include <iostream>

std::timed_mutex tmutex;

void try_with_timeout()
{
    if (tmutex.try_lock_for(std::chrono::milliseconds(100)))
    {
        std::cout << "acquired the lock\n";
        tmutex.unlock();
    }
    else
    {
        std::cout << "gave up waiting for the lock\n";
    }
}

Condition Variables

A mutex protects data, but it does not by itself let one thread efficiently wait for another thread to make some condition true — repeatedly locking a mutex just to check a flag (busy-waiting) wastes CPU. std::condition_variable lets a thread sleep until another thread explicitly wakes it, without polling.

consumer calls cv.wait(lock, predicate) while already holding the lock (via unique_lock, which wait() requires so it can unlock while sleeping and relock before returning).

wait() atomically unlocks the mutex and puts the thread to sleep, so other threads can acquire the mutex while consumer waits.

producer pushes a value, then calls cv.notify_one() to wake one waiting thread.

When woken, wait() reacquires the lock and re-checks the predicate (!work_queue.empty() || done); if it is still false it goes back to sleep — this guards against spurious wakeups, which the standard explicitly permits.

Once the predicate is true, wait() returns with the lock held, and consumer drains the queue.

The predicate passed to wait() is what makes this correct even with spurious wakeups (the OS is allowed to wake a waiting thread without notify_one/notify_all ever being called): wait() simply re-checks the predicate and goes back to sleep if it is still false, rather than assuming a wakeup means the condition is true.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex m;
std::condition_variable cv;
std::queue<int> work_queue;
bool done = false;

void producer()
{
    for (int i = 1; i <= 3; ++i)
    {
        {
            std::lock_guard<std::mutex> lock(m);
            work_queue.push(i);
        }
        cv.notify_one(); // wake the consumer
    }
    {
        std::lock_guard<std::mutex> lock(m);
        done = true;
    }
    cv.notify_one();
}

void consumer()
{
    while (true)
    {
        std::unique_lock<std::mutex> lock(m);
        cv.wait(lock, [] { return !work_queue.empty() || done; });

        while (!work_queue.empty())
        {
            std::cout << "consumed: " << work_queue.front() << "\n";
            work_queue.pop();
        }
        if (done)
            break;
    }
}

int main()
{
    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();
}

Futures, Promises, and std::async

std::thread has no built-in way to return a value or propagate an exception back to the launching thread. std::future, std::promise, and std::async fill that gap: a promise is written to on one thread, a future is read from (possibly on a different thread), and the two are connected to the same shared state — reading the future blocks until the promise's value is ready.

std::async(std::launch::async, compute_square, 6) requests that compute_square(6) run asynchronously (on a separate thread, as opposed to std::launch::deferred, which would run it lazily on whichever thread calls get()). result.get() blocks the calling thread only until the value is actually ready, and returns it directly — no shared global variable or manual mutex is needed to move the result across threads. Calling get() a second time on the same future is undefined behavior; a future's result can only be retrieved once.

std::promise is the lower-level building block std::async uses internally — useful when you need to hand a result back from code that is not itself a simple callable std::async can wrap directly.

p.get_future() creates the future connected to p's shared state before p is moved into the new thread — std::promise is move-only, so it must be transferred with std::move. f.get() in main() blocks until worker calls result_promise.set_value(42) on the other thread, at which point it unblocks and returns 42, giving main() a value that was actually computed on a different thread without any manually managed shared variable.

#include <iostream>
#include <future>

int compute_square(int x)
{
    return x * x;
}

int main()
{
    // std::async launches compute_square (often on a new thread) and
    // immediately returns a future representing its eventual result.
    std::future<int> result = std::async(std::launch::async, compute_square, 6);

    std::cout << "doing other work while it computes...\n";

    int value = result.get(); // blocks until compute_square finishes
    std::cout << "square: " << value << "\n";
}
#include <iostream>
#include <thread>
#include <future>

void worker(std::promise<int> result_promise)
{
    int computed = 21 * 2;
    result_promise.set_value(computed); // fulfills the promise
}

int main()
{
    std::promise<int> p;
    std::future<int> f = p.get_future();

    std::thread t(worker, std::move(p));

    std::cout << "result: " << f.get() << "\n"; // blocks until set_value runs
    t.join();
}

Best Practices

Always join or detach every std::thread before it is destroyed — never let one go out of scope while still joinable. Prefer RAII lock types (lock_guard, unique_lock, scoped_lock) over manual lock()/unlock() pairs, since a lock that is only released by an explicit unlock() call is skipped by any early return or exception in between. Lock the minimum data for the minimum time — holding a mutex across expensive work (I/O, a long computation) needlessly blocks every other thread that wants it. Use scoped_lock (or std::lock) whenever more than one mutex must be held at once, to avoid ordering deadlocks. Never wait() on a condition variable without a predicate unless you have a very specific reason to, since spurious wakeups are allowed by the standard. And prefer std::async and futures over raw threads plus manually shared variables whenever the goal is simply 'run this and give me the result back' — it removes an entire category of synchronization bugs.

What happens if a std::thread object is destroyed while it is still joinable (neither joined nor detached), and why does the standard make this a hard error rather than an implicit join?

std::terminate() is called and the program aborts immediately. The standard deliberately does not implicitly join, because a destructor silently blocking (potentially for a long or unbounded time) would be a surprising and dangerous default; requiring an explicit join() or detach() forces the programmer to consciously decide what should happen to the thread.

Why does passing an int& parameter to a function launched via std::thread not actually let that function modify the caller's variable, unless std::ref is used?

std::thread's constructor copies (or moves) its arguments into internal storage for the new thread, regardless of what type the target function expects — this is a safety measure so the calling thread's variables cannot be invalidated mid-use by the new thread. A plain int& parameter ends up bound to that internal copy, not the caller's original variable. std::ref(value) wraps the argument in a std::reference_wrapper, which is copied instead of the underlying object, while still referring back to the original variable.

Formally, what three conditions together constitute a data race in C++, and what does the standard say happens when one occurs?

A data race occurs when two or more threads access the same memory location, at least one of those accesses is a write, and there is no synchronization establishing a happens-before ordering between the accesses. The C++ standard specifies that a program containing a data race has undefined behavior — not merely an unpredictable value, but no guarantees about the program's behavior at all.

Why must a condition variable's wait() call always be given a predicate (or manually re-check a condition after waking), rather than assuming a wakeup means the awaited condition is now true?

The C++ standard permits spurious wakeups — a waiting thread can wake up even though no thread ever called notify_one() or notify_all(), and even though the awaited condition may still be false. Passing a predicate to wait() makes it re-check that condition after every wakeup and go back to sleep if it is still false, which is the only way to handle spurious wakeups correctly.

What is the key difference between std::launch::async and std::launch::deferred as policies for std::async, and what does calling .get() do differently between the two?

std::launch::async requires the callable to run on a separate thread of execution, starting essentially immediately. std::launch::deferred instead delays execution until .get() (or .wait()) is actually called on the returned future, at which point the callable runs synchronously on the calling thread itself, not on a separate thread at all. Both eventually make the result available through the same future interface, but deferred execution means the work does not happen concurrently unless something forces it to.

Write a C++ program that launches three std::thread objects, each printing its own thread id along with a distinct message, and joins all three before main() exits.

Each thread runs worker(i) concurrently with a distinct id (1, 2, 3) and its own OS thread id from std::this_thread::get_id(). Building the message in a local std::ostringstream before a single std::cout << oss.str() call avoids interleaving individual << operations from different threads into a garbled line. The final for loop joins all three threads, guaranteeing 'all workers finished' only prints after every worker has actually completed; the relative order of the three worker lines themselves is not guaranteed and can vary between runs.

What is wrong with this program, what is likely to happen when it runs, and how would std::scoped_lock fix it?

This is a classic lock-ordering deadlock: thread_a locks m1 then tries to lock m2, while thread_b locks m2 then tries to lock m1, in the opposite order. With the added sleep making the timing likely to overlap, thread_a can end up holding m1 and waiting for m2 while thread_b holds m2 and waits for m1 — neither can proceed, and the program hangs forever instead of printing either 'done' message. Replacing each function's two separate lock_guards with a single std::scoped_lock lock(m1, m2); (locking both mutexes together via a deadlock-avoiding algorithm) fixes it, since neither thread can ever end up holding only one of the two mutexes while waiting on the other.

Write a C++ program using a std::mutex and a std::condition_variable to implement a single-slot 'mailbox': one thread (the sender) sets a message and notifies, and another thread (the receiver) waits for a message to arrive and then prints it.

receiver locks the mutex and calls cv.wait with a predicate checking mailbox.has_value(), so it sleeps efficiently instead of busy-polling, and correctly handles any spurious wakeup by re-checking the predicate. sender sleeps briefly to simulate work, then locks the same mutex to set mailbox under protection, unlocks (the lock_guard's scope ends), and calls notify_one() to wake the receiver. Once woken with the predicate now true, receiver reacquires the lock, wait() returns, and it prints 'received: hello from sender' regardless of exactly how the two threads were scheduled.

What does this program compute and print, and why is calling f1.get() before f2.get() safe even though the two async tasks may finish in either order?

It computes the sum of all integers from 0 up to (but not including) 1,000,000 by splitting the range in half and summing each half concurrently via std::async, printing 499999500000. Calling f1.get() first is safe regardless of which task actually finishes first, because get() simply blocks until its own future's result is ready — it does not require f1's task to have started or finished before f2's. Whichever order the two tasks complete in internally, f1.get() + f2.get() always waits for both and adds their two correct partial sums together.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1;
std::mutex m2;

void thread_a()
{
    std::lock_guard<std::mutex> lock1(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock2(m2);
    std::cout << "thread_a done\n";
}

void thread_b()
{
    std::lock_guard<std::mutex> lock2(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock1(m1);
    std::cout << "thread_b done\n";
}

int main()
{
    std::thread a(thread_a);
    std::thread b(thread_b);
    a.join();
    b.join();
}
#include <iostream>
#include <future>
#include <vector>
#include <numeric>

long long sum_range(int start, int end)
{
    long long total = 0;
    for (int i = start; i < end; ++i)
        total += i;
    return total;
}

int main()
{
    auto f1 = std::async(std::launch::async, sum_range, 0, 500000);
    auto f2 = std::async(std::launch::async, sum_range, 500000, 1000000);

    long long total = f1.get() + f2.get();
    std::cout << total << "\n";
}