C++ Multithreading: Relaxed Ordering and the Torn Read Bug
Orders duplicated/lost every 12-15 hours under 100k orders/min.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- C++ multithreading lets multiple code paths run concurrently on separate cores
- std::thread creates OS threads; join() blocks until completion
- std::mutex protects shared data; lock()/unlock() must be paired
- std::atomic provides lock-free reads/writes for simple counters
- Condition variables avoid busy-waiting; always pair with a predicate
- Memory ordering (seq_cst, acquire, release) controls visibility across threads
Multithreading means executing multiple sequences of instructions concurrently. In C++, the standard library provides std::thread since C++11, which wraps the OS thread API (pthreads on Linux, WinThreads on Windows). Each std::thread object represents a single thread of execution. You launch a thread by passing a callable — a function, lambda, or functor — to the constructor.
The key trade-off: threads share the same address space. This makes data sharing cheap (just a pointer) but introduces race conditions when two threads modify the same data without synchronization. Here's the minimal example that actually runs work in parallel:
Imagine a busy restaurant kitchen. One chef doing everything — chopping, boiling, plating — is single-threaded. Now picture five chefs working simultaneously: one chops, one stirs, one plates. That's multithreading. The magic happens fast, but chaos breaks out if two chefs reach for the same knife at the same time — that's a race condition. A mutex is the rule that says 'only one chef touches the knife block at a time.'
Modern CPUs ship with 8, 16, even 64 cores, and most C++ programs use exactly one of them. That's like buying a Formula 1 car and driving it in second gear. Multithreading is how you put all that hardware to work — and in latency-sensitive systems like game engines, financial trading platforms, and real-time data pipelines, it's the difference between a product that ships and one that gets cancelled.
The problem multithreading solves is deceptively simple: some work can happen in parallel, so make it happen in parallel. But the devil is in the details. Shared mutable state, non-obvious memory visibility, spurious wakeups, priority inversion, and the C++ memory model's acquire-release semantics make this one of the hardest topics in the language to get right in production. Getting it wrong doesn't just cause bugs — it causes bugs that only appear under load, on specific hardware, once a month.
By the end of this article you'll understand how std::thread works under the hood, why std::mutex costs what it costs, when to reach for std::atomic instead, how condition variables enable efficient thread coordination without spinning, and what the C++ memory model actually guarantees. You'll leave with patterns you can deploy in real codebases today.
What Is Multithreading in C++?
Multithreading means executing multiple sequences of instructions concurrently. In C++, the standard library provides std::thread since C++11, which wraps the OS thread API (pthreads on Linux, WinThreads on Windows). Each std::thread object represents a single thread of execution. You launch a thread by passing a callable — a function, lambda, or functor — to the constructor.
The key trade-off: threads share the same address space. This makes data sharing cheap (just a pointer) but introduces race conditions when two threads modify the same data without synchronization. Here's the minimal example that actually runs work in parallel:
#include <iostream> #include <thread> #include <vector> namespace io::thecodeforge::multithreading { void worker(int id) { std::cout << "Thread " << id << " running on core " << sched_getcpu() << '\n'; } void launch_workers() { std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) threads.emplace_back(worker, i); for (auto& t : threads) t.join(); } } // namespace io::thecodeforge::multithreading int main() { io::thecodeforge::multithreading::launch_workers(); }
- std::thread is a RAII wrapper around pthread_create / CreateThread.
- join() blocks the calling thread until the worker finishes.
- detach() lets the thread run independently — but you lose control.
- Always join or detach every thread. The destructor of a joinable thread calls std::terminate.
Callable Types for std::thread Constructor: Comparison Table
std::thread can be constructed with any callable type. The table below compares the four common categories: free functions, lambda expressions, functors (function objects), and member function pointers. Each has distinct syntax and typical use cases.
| Callable Type | Syntax Example | Notes |
|---|---|---|
| Free function | std::thread t(func, arg1, arg2); | Simple, but cannot capture state easily. |
| Lambda | std::thread t([capture]{ / code / }); | Most flexible; can capture by value or reference. Prefer for short tasks. |
| Functor | std::thread t(std::ref(myFunctor)); | Useful when stateful callable is needed across multiple invocations. |
| Member function | std::thread t(&MyClass::method, &obj, args); | Common in OOP designs; must ensure object outlives thread. |
Here's a complete demonstration of all four:
#include <iostream> #include <thread> namespace io::thecodeforge::multithreading { // 1. Free function void free_func(int x) { std::cout << "Free function: " << x << '\n'; } // 2. Functor struct Functor { void operator()(int x) const { std::cout << "Functor: " << x << '\n'; } }; // 3. Class with member function class Worker { public: void method(int x) const { std::cout << "Member function: " << x << '\n'; } }; void launch_all() { // Free function std::thread t1(free_func, 1); // Lambda std::thread t2([](int x){ std::cout << "Lambda: " << x << '\n'; }, 2); // Functor Functor f; std::thread t3(f, 3); // Member function Worker w; std::thread t4(&Worker::method, &w, 4); t1.join(); t2.join(); t3.join(); t4.join(); } } // namespace int main() { io::thecodeforge::multithreading::launch_all(); }
this pointer.Mutexes: The Last Line of Defense Against Races
A mutex (mutual exclusion) ensures that only one thread executes a critical section at a time. C++ offers std::mutex
#include <iostream> #include <thread> #include <mutex> #include <vector> namespace io::thecodeforge::multithreading { class SafeCounter { int counter_ = 0; std::mutex mtx_; public: void increment() { std::lock_guard<std::mutex> lock(mtx_); ++counter_; } int get() const { std::lock_guard<std::mutex> lock(mtx_); return counter_; } }; void test() { SafeCounter sc; std::vector<std::thread> threads; for (int i = 0; i < 100; ++i) threads.emplace_back(&SafeCounter::increment, &sc); for (auto& t : threads) t.join(); std::cout << "Final count: " << sc.get() << '\n'; } } // namespace int main() { io::thecodeforge::multithreading::test(); }
try_lock_for().Mutex Types Comparison Table
C++ provides several mutex types tailored for specific scenarios. The table below compares std::mutex, std::timed_mutex, std::shared_mutex, and std::recursive_mutex across key attributes.
| Mutex Type | Reentrant | Timed Lock | Reader/Writer | Overhead (uncontested) |
|---|---|---|---|---|
| std::mutex | No | No | No | Lowest (~25ns) |
| std::timed_mutex | No | Yes (try_lock_for/until) | No | Low (~30ns) |
| std::recursive_mutex | Yes | No | No | Moderate (~35ns) |
| std::shared_mutex | No | No | Yes | Higher (~50ns for write, ~30ns for read) |
std::shared_mutex is especially useful for read-heavy workloads where multiple readers can proceed simultaneously without blocking each other. Here's an example of using std::shared_mutex with a reader-writer lock pattern:
#include <iostream> #include <shared_mutex> #include <thread> #include <vector> namespace io::thecodeforge::multithreading { class ThreadSafeCache { mutable std::shared_mutex mtx_; int cached_value_ = 0; public: void write(int val) { std::unique_lock lock(mtx_); cached_value_ = val; } int read() const { std::shared_lock lock(mtx_); return cached_value_; } }; void test() { ThreadSafeCache cache; std::thread writer([&]{ cache.write(42); }); std::vector<std::thread> readers; for (int i = 0; i < 10; ++i) readers.emplace_back([&]{ std::cout << cache.read() << ' '; }); writer.join(); for (auto& t : readers) t.join(); } } // namespace int main() { io::thecodeforge::multithreading::test(); }
Atomics: Lock-Free Data Sharing Done Right
std::atomic<T> provides lock-free operations for integer types (and pointers) on most platforms. Atomics use CPU instructions like x86 LOCK prefix or CMPXCHG to ensure atomic reads and writes without a mutex. They also control memory ordering to enforce visibility guarantees.
The critical difference: a normal variable can be torn during a read if another thread writes simultaneously. An atomic variable guarantees that loads and stores are indivisible. But correctness also requires proper memory ordering — the default std::memory_order_seq_cst is safest but slowest.
#include <iostream> #include <atomic> #include <thread> #include <vector> namespace io::thecodeforge::multithreading { std::atomic<int> counter{0}; void increment() { // memory_order_relaxed is sufficient for a counter that's eventually consistent counter.fetch_add(1
Condition Variables: Efficient Thread Notification
A condition variable allows one thread to wait for a condition to become true without busy-waiting. std::condition_variable must be paired with a std::unique_lock<std::mutex> and a predicate. The pattern: the waiting thread calls wait(lock, predicate), which atomically unlocks the mutex and blocks. When another thread calls notify_one() or notify_all(), the waiting thread re-acquires the mutex and re-checks the predicate.
The predicate is critical — it prevents spurious wakeups (which occur even on POSIX systems). Without a predicate, the waiting thread might wake up even though the condition isn't true, leading to logic bugs.
#include <iostream> #include <condition_variable> #include <mutex> #include <queue> #include <thread> namespace io::thecodeforge::multithreading { std::queue<int> messages; std::mutex mtx; std::condition_variable cv; void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); { std::lock_guard<std::mutex> lock(mtx); messages.push(i); } cv.notify_one(); } } void consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return !messages.empty(); }); int val = messages.front(); messages.pop(); lock.unlock(); std::cout << "Consumed: " << val << '\n'; if (val == 9) break; } } } // namespace int main() { std::thread p(io::thecodeforge::multithreading::producer); std::thread c(io::thecodeforge::multithreading::consumer); p.join(); c.join(); }
wait() unless you have a separate check loop.notify_one() and check if work remains.wait() to handle spurious wakeups.notify_all() for broadcast.wait_for() with a timeout, or a polling loop with std::this_thread::sleep_for().Launching Asynchronous Tasks with std::async and std::future
std::async provides a higher-level interface for parallel tasks. It returns a std::future which will hold the result once the task completes. Unlike std::thread, you don't need to manage thread lifetime manually — the future's destructor will join or detach the task depending on the launch policy.
- std::launch::async: The task runs on a new thread immediately.
- std::launch::deferred: The task is executed lazily when
get()orwait()is called, on the calling thread.
The default policy (std::launch::async | std::launch::deferred) lets the implementation choose, which can lead to surprising sequential execution. Always specify std::launch::async explicitly if you want parallelism.
#include <iostream> #include <future> #include <chrono> namespace io::thecodeforge::multithreading { int slow_square(int x) { std::this_thread::sleep_for(std::chrono::seconds(1)); return x * x; } void example() { // Launch two tasks asynchronously std::future<int> f1 = std::async(std::launch::async, slow_square, 5); std::future<int> f2 = std::async(std::launch::async, slow_square, 7); // Do other work while tasks run... std::cout << "Waiting for results...\n"; // Get results (blocks until each completes) int result1 = f1.get(); int result2 = f2.get(); std::cout << "Results: " << result1 << ", " << result2 << '\n'; } } // namespace int main() { io::thecodeforge::multithreading::example(); }
get() on the future will execute the task synchronously. Worse, if you destroy the future without calling get(), the destructor blocks until the task completes if deferred. To avoid surprises, always specify std::launch::async when you need concurrency.get() to retrieve the result; the future destructor will join/deferred-execute if not called.Memory Ordering and the C++ Memory Model
The C++ memory model defines how operations on different threads become visible to each other. Without proper ordering, a thread might see stale values or operations appear to happen in a different order than written. The model is built on happens-before relationships: operation A happens-before operation B if B must see A's effects.
std::atomic provides six memory order modes: memory_order_relaxed (no ordering constraints), memory_order_consume (deprecated), memory_order_acquire (reads cannot be reordered before this point), memory_order_release (writes cannot be reordered after this point), memory_order_acq_rel (acquire+release for read-modify-write), and memory_order_seq_cst (sequential consistency — default). Acquire-release pairs create happens-before edges.
#include <atomic> #include <thread> #include <cassert> namespace io::thecodeforge::multithreading { std::atomic<int> data{0}; std::atomic<int> flag{0}; void writer() { data.store(42
- release: changes propagate to other caches after this store completes.
- acquire: all previous writes from the releasing thread are guaranteed visible.
- seq_cst: the strongest ordering — every thread sees the same order of operations.
- relaxed: no ordering — only atomicity is guaranteed. Use only for counters with eventual consistency.
Thread Synchronization Primitives Summary Table
C++ provides a rich set of synchronization primitives for different coordination patterns. The table below summarizes the most common ones, including those from C++11 (mutex, atomic, condition_variable, future) and newer additions from C++20 (semaphore, barrier, latch).
| Primitive | Header | Purpose | Key API | Blocking |
|---|---|---|---|---|
| std::mutex | <mutex> | Mutual exclusion for critical sections | lock() / unlock() | Yes |
| std::shared_mutex | <shared_mutex> | Multiple readers, single writer | lock_shared() / lock() | Yes |
| std::atomic<T> | <atomic> | Lock-free operations on single variables | load() / store() / fetch_add() | No (may spin) |
| std::condition_variable | <condition_variable> | Block thread until condition is true | wait() / notify_one() | Yes |
| std::future / std::promise | <future> | Retrieve value from asynchronous task | get() / set_value() | Yes on get() |
| std::counting_semaphore | <semaphore> | Resource counting (C++20) | acquire() / release() | Yes |
| std::barrier | <barrier> | Synchronize phases among threads (C++20) | arrive_and_wait() | Yes |
| std::latch | <latch> | One-time synchronization point (C++20) | count_down() / wait() | Yes |
For most applications, the first five primitives cover 90% of needs. The C++20 primitives reduce boilerplate in multi-phase parallel algorithms.
Thread Pool Pattern: Capping Concurrency
Creating and destroying threads for every task has significant overhead and can overwhelm the system. A thread pool maintains a fixed number of worker threads that continuously pull tasks from a shared queue. This caps concurrency, reduces latency, and prevents resource exhaustion.
Below is a minimal thread pool implementation using std::thread, std::mutex, std::condition_variable, and std::queue. Workers run an infinite loop: they wait for a task on the queue, execute it, then check for new work. The pool enqueues tasks via push_task().
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> #include <functional> #include <vector> namespace io::thecodeforge::multithreading { class ThreadPool { std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex mtx; std::condition_variable cv; bool stop = false; public: explicit ThreadPool(size_t count) { for (size_t i = 0; i < count; ++i) workers.emplace_back([this] { while (true) { std::function<void()> task; { std::unique_lock lock(mtx); cv.wait(lock, [this]{ return stop || !tasks.empty(); }); if (stop && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); } task(); } }); } ~ThreadPool() { { std::lock_guard lock(mtx); stop = true; } cv.notify_all(); for (auto& w : workers) w.join(); } template <class F> void push_task(F&& f) { { std::lock_guard lock(mtx); tasks.emplace(std::forward<F>(f)); } cv.notify_one(); } }; void example() { ThreadPool pool(4); // 4 workers for (int i = 0; i < 10; ++i) pool.push_task([i] { std::cout << "Task " << i << " on thread " << std::this_thread::get_id() << '\n'; }); std::this_thread::sleep_for(std::chrono::seconds(1)); } // pool destructor joins all threads } // namespace int main() { io::thecodeforge::multithreading::example(); }
hardware_concurrency() for CPU-bound work.Thread Detachment: The Fire-and-Forget Footgun
You don't always want to join. Sometimes you need a thread to live on its own — logging, monitoring, a background heartbeat — while your main thread moves on. That's std::thread::detach().
Detaching means you relinquish ownership. The OS takes over, and the thread runs independently until it finishes. You can't join it anymore. You can't check its status. The thread is a ghost.
Production reality: detach is dangerous if your thread accesses stack variables from the parent scope. The parent might unwind before the thread reads them. Classic use-after-free. If you detach, make sure your thread owns its data or uses heap-allocated resources managed by std::shared_ptr.
Never detach without understanding that will return false afterward. Calling join on a detached thread crashes your program. The rule: attach your thread to a scope (joinable()join) or detach it explicitly. Either way, one of them must happen. No exceptions.
// io.thecodeforge — c-cpp tutorial #include <thread> #include <iostream> #include <chrono> void backgroundLogger() { for (int i = 0; i < 3; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Log: heartbeat " << i << "\n"; } } int main() { std::thread logger(backgroundLogger); logger.detach(); // fire and forget // Main thread continues immediately std::cout << "Main continues...\n"; std::this_thread::sleep_for(std::chrono::milliseconds(350)); std::cout << "Main done. Logger might still be running.\n"; return 0; }
main() exits before the detached thread finishes, the thread is abruptly terminated. No cleanup runs. Use detach only for threads that can die without consequence.Thread IDs: Identifying Your Workers in the Zoo
When you have 20 worker threads hammering a queue, you need to know which thread is printing that garbled log line. std::this_thread::get_id() returns a unique std::thread::id for every running thread.
You can store IDs in a set, print them for debugging, or use them as keys in thread-local storage maps. They're hashable, comparable, and copyable. They're your threads' fingerprints.
Senior trade secret: don't rely on thread IDs for security or persistence. The OS can recycle IDs after threads exit. They're unique only during the thread's lifetime. Use them for logging, profiling, or ensuring a critical section is only entered by one specific thread (bad idea — use a mutex instead).
Also: std::thread::id has a default constructor that yields a special 'not-a-thread' ID. Useful for optional thread ownership patterns. Compare with == or sort them into maps. It's a proper value type.
// io.thecodeforge — c-cpp tutorial #include <iostream> #include <thread> #include <vector> void work(int taskId) { auto id = std::this_thread::get_id(); std::cout << "Task " << taskId << " on thread " << id << "\n"; } int main() { std::vector<std::thread> workers; for (int i = 0; i < 4; ++i) { workers.emplace_back(work, i); } std::cout << "Main thread id: " << std::this_thread::get_id() << "\n"; for (auto& t : workers) { t.join(); } return 0; }
Callables Beyond Functions: Lambdas, Functors, and Member Functions
You're not limited to plain functions when constructing std::thread. The constructor accepts anything callable — lambdas, function objects, member functions, even std::bind results. This shapes how you capture state and manage lifetimes.
Lambdas are the default choice in modern C++. They capture variables by value or reference. Capture by reference is dangerous if the lambda executes after the captured variable goes out of scope. Capture by value is safe but copies everything. Move semantics ([ptr = std::move(ptr)]) avoid copying while being safe.
Member functions require a pointer to the object and the arguments. Syntax: std::thread(&Class::method, &instance, args...). The pointer is passed as the second argument. Be careful — if the instance gets destroyed before the thread finishes, you're dereferencing a ghost.
Functor classes (operator()) let you pack complex state into one object. They're slower to write but useful when you need RAII wrappers for thread resources. Pick the callable type that makes the lifetime contract explicit: lambda for quick one-offs, functor for reusable thread tasks, member function for OOP integration.
// io.thecodeforge — c-cpp tutorial #include <iostream> #include <thread> class Worker { public: void process(int id) { std::cout << "Member on " << id << "\n"; } }; struct Functor { void operator()(int x) { std::cout << "Functor got " << x << "\n"; } }; int main() { Worker w; std::thread t1(&Worker::process, &w, 1); // member Functor f; std::thread t2(f, 2); // functor std::thread t3([](int x) { // lambda std::cout << "Lambda with " << x << "\n"; }, 3); t1.join(); t2.join(); t3.join(); return 0; }
Context Switch: The Performance Tax You Can't Dodge
A context switch is when the OS yanks a thread off the CPU and piles another one on. It's not free. The kernel saves registers, flushes TLBs, reloads new state — that's microseconds of dead time. Do that thousands of times per second and your throughput tanks.
Why should you care? Because most devs think "more threads = faster." Nope. If your threads outnumber CPU cores and they fight over mutexes, you burn cycles on switching instead of working. The fix: keep thread count close to core count. Use a thread pool (already covered) and batch work into chunks big enough to amortize the switch cost. Measure context switch rate with perf or top -H. If it's spiking, your design is wrong.
Production rule: one context switch per chunk of real work isn't a problem. A hundred switches per lock acquisition? You're leaking throughput.
// io.thecodeforge — c-cpp tutorial // Demo showing high context switch overhead #include <thread> #include <vector> #include <mutex> #include <iostream> std::mutex m; int shared = 0; void hammer() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lg(m); ++shared; // Tiny critical section } } int main() { const int num_threads = 8; std::vector<std::thread> threads; for (int i = 0; i < num_threads; ++i) threads.emplace_back(hammer); for (auto& t : threads) t.join(); std::cout << "Final count: " << shared << std::endl; return 0; }
Example 1: Email Server — Multithreaded Queue Popping
An email server receives thousands of messages per second. Each message needs parsing, spam checking, and routing to a mailbox. You cannot block the network listener for any of that. So you push the raw message onto a concurrent queue and let worker threads pop and process.
The pattern: one producer thread (or more from I/O), N consumer threads. The queue protects itself with a mutex and condition variable (see previous sections). The key insight: never hold the queue lock while processing. Pop the item, release the lock, then do the heavy lifting. Holding the lock across disk I/O or spam filtering turns your concurrency into a serial bottleneck.
This example shows a bounded queue with a single producer and two consumers. In production you'd tune consumer count to core count and measure queue depth to avoid memory blowup.
// io.thecodeforge — c-cpp tutorial // Minimal email server: push raw emails, pop and process #include <queue> #include <mutex> #include <condition_variable> #include <thread> #include <iostream> #include <string> struct Inbox { int id; std::string raw; }; class MailQueue { std::queue<Inbox> q_; std::mutex m_; std::condition_variable cv_; public: void push(Inbox msg) { std::lock_guard<std::mutex> lk(m_); q_.push(std::move(msg)); cv_.notify_one(); } Inbox pop() { std::unique_lock<std::mutex> lk(m_); cv_.wait(lk, [this]{ return !q_.empty(); }); Inbox msg = std::move(q_.front()); q_.pop(); return msg; } }; int main() { MailQueue mq; auto worker = [&]{ while(true) { auto msg = mq.pop(); std::cout << "Processing msg " << msg.id << std::endl; }}; auto producer = [&]{ for(int i=0;;++i) mq.push({i, "raw email"}); }; std::thread t1(worker), t2(worker); producer(); }
The Hidden Race That Killed Our Trading Engine at 2 AM
- Never assume relaxed ordering is safe just because your code looks correct.
- Always pair release stores with acquire loads when sharing data between threads.
- Test under sustained load with multiple CPU sockets to expose ordering issues.
lock() calls.g++ -fsanitize=thread -g program.cpp -o program && ./programvalgrind --tool=helgrind ./programgdb -p $(pgrep myapp) -batch -ex 'thread apply all bt' -ex quitlsof -p $(pgrep myapp) | grep mutexperf stat -e cache-misses,cache-references ./programobjdump -d program | grep -A5 'lock add'| Primitive | Overhead (contested) | Best For | Pitfall |
|---|---|---|---|
| std::thread | ~30μs to spawn | Long-running parallel tasks | Must join/detach; oversubscription |
| std::mutex | ~25ns → 10μs | Protecting critical sections | Deadlocks; contention kills performance |
| std::atomic<T> | ~5ns (relaxed) | Simple shared states (counter, flag) | Does not compose; ordering errors |
| condition_variable | ~5μs wake latency | Event-driven waiting | Spurious wakeups; must use predicate |
| File | Command / Code | Purpose |
|---|---|---|
| io | namespace io::thecodeforge::multithreading { | What Is Multithreading in C++? |
| io | namespace io::thecodeforge::multithreading { | Callable Types for std |
| io | namespace io::thecodeforge::multithreading { | cpp configuration |
| io | namespace io::thecodeforge::multithreading { | Mutex Types Comparison Table |
| io | namespace io::thecodeforge::multithreading { | Atomics |
| io | namespace io::thecodeforge::multithreading { | Condition Variables |
| io | namespace io::thecodeforge::multithreading { | Launching Asynchronous Tasks with std |
| io | namespace io::thecodeforge::multithreading { | Memory Ordering and the C++ Memory Model |
| io | namespace io::thecodeforge::multithreading { | Thread Pool Pattern |
| DetachLogger.cpp | void backgroundLogger() { | Thread Detachment |
| ThreadIdTracker.cpp | void work(int taskId) { | Thread IDs |
| CallableVariety.cpp | class Worker { | Callables Beyond Functions |
| ContextSwitchDemo.cpp | std::mutex m; | Context Switch |
| EmailServerPop.cpp | struct Inbox { int id; std::string raw; }; | Example 1: Email Server |
Key takeaways
wait().Common mistakes to avoid
4 patternsUsing std::atomic without memory ordering
Locking multiple mutexes in different order across threads
Not protecting reads of shared variables
Calling notify_one() without holding the mutex
cv.notify_one();Interview Questions on This Topic
What is a data race and how does it differ from a race condition?
Explain the difference between memory_order_release and memory_order_seq_cst. When would you use each?
How would you implement a thread-safe counter without using a mutex?
What is false sharing and how do you mitigate it?
Frequently Asked Questions
A mutex is tied to a thread: the thread that locks it must unlock it. A semaphore can be signalled by any thread. In C++, use std::mutex for mutual exclusion and std::counting_semaphore (C++20) for resource counting. Mutexes implement priority inheritance on some systems to avoid priority inversion; semaphores typically do not.
Only trivially copyable types are guaranteed to have atomic support via std::atomic<T>. For larger types, the compiler may fall back to a mutex (using the lock-free() query). In practice, limit atomics to integer types, enums, and pointers.
A spurious wakeup is when a condition variable wait returns even though the predicate is false. It's allowed by POSIX to simplify implementation. Handle it by always waiting with a predicate: cv.wait(lock, []{ return predicate; }); or wrapping wait() in a loop that checks the predicate.
std::async returns a std::future and is simpler for launching background tasks when you need a result. Use std::thread when you need explicit control over thread lifecycle, affinity, or priority. std::async may or may not create a separate thread depending on the launch policy (std::launch::async guarantees a new thread).
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C++ Advanced. Mark it forged?
7 min read · try the examples if you haven't