Multithreading in C++
A hands-on introduction to multithreading in C++ — how to launch threads with std::thread, why race conditions happen, and how mutexes and lock guards keep shared data safe.

Modern CPUs come with multiple cores, but a single-threaded program only ever uses one of them at a time. Multithreading is how a C++ program puts the rest of those cores to work — running multiple pieces of code genuinely in parallel, rather than one after another.
Launching a Thread
Since C++11, std::thread is the standard way to run a function concurrently.
cpp#include <iostream> #include <thread> void printNumbers() { for (int i = 1; i <= 5; i++) { std::cout << i << " "; } } int main() { std::thread t(printNumbers); // starts running concurrently t.join(); // wait for it to finish before continuing std::cout << "\nDone!"; }
t.join() blocks main() until the thread finishes. Skipping it (or calling t.detach() instead, which lets the thread run independently) is a common source of bugs if the program exits before the thread completes.
The Core Problem: Race Conditions
Multithreading's real difficulty isn't launching threads — it's what happens when two threads touch the same data at the same time.
cppint counter = 0; void increment() { for (int i = 0; i < 100000; i++) { counter++; // NOT thread-safe } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << counter; // often NOT 200000 }
counter++ looks like one operation, but it's actually three: read the value, increment it, write it back. If both threads read the same value before either writes back, one increment gets silently lost. This is a race condition, and it's the central hazard of concurrent programming.
Fixing It with a Mutex
A mutex (mutual exclusion lock) ensures only one thread can execute a section of code at a time.
cpp#include <mutex> std::mutex mtx; int counter = 0; void increment() { for (int i = 0; i < 100000; i++) { mtx.lock(); counter++; mtx.unlock(); } }
Now, while one thread holds the lock, any other thread trying to lock() the same mutex simply waits until it's released — eliminating the race.
lock_guard: Locking the RAII Way
Manually pairing lock() and unlock() has the same risk as manual new/delete — forget one, or throw an exception in between, and the mutex stays locked forever. std::lock_guard applies RAII here too: it locks on construction and unlocks automatically when it goes out of scope.
cppvoid increment() { for (int i = 0; i < 100000; i++) { std::lock_guard<std::mutex> lock(mtx); counter++; } // mutex automatically unlocked here, even if an exception is thrown }
This is the version you should actually write in real code — manual lock()/unlock() calls are best avoided entirely.
A Brief Note on Condition Variables
For situations where one thread needs to wait for another thread to signal that something is ready — rather than just avoiding simultaneous access — std::condition_variable lets a thread sleep efficiently until it's notified, instead of repeatedly checking a flag in a loop (a wasteful pattern called "busy-waiting").
Common Pitfalls
- Forgetting to
join()ordetach()a thread before it goes out of scope, which crashes the program. - Deadlocks — two threads each waiting on a lock the other one holds, so neither ever proceeds. Consistent lock ordering across your codebase is the usual prevention.
- Over-threading — spinning up far more threads than CPU cores, which adds overhead from constant context-switching instead of real parallelism.
The Takeaway
Multithreading unlocks real parallel performance, but every bit of shared, mutable data touched by more than one thread becomes a potential race condition. std::thread gets code running concurrently; std::mutex (ideally wrapped in lock_guard) is what keeps shared data consistent while it does.

