Chapter 19: Multithreading & Concurrency
Multithreading & Concurrency in C++: Running Multiple Tasks at the Same Time!
Hello my fantastic student! π Welcome to Lesson 19 β Multithreading & Concurrency β one of the most powerful, most exciting, and also most dangerous topics in modern C++!
In todayβs world, almost every serious application (games, servers, data processing, AI training, video editingβ¦) uses multiple threads to do many things at the same time and make the best use of multi-core CPUs.
C++ gives us excellent tools starting from C++11 to write safe, efficient multi-threaded code.
Today weβll cover everything in great detail:
- std::thread β creating and managing threads
- Mutex + lock_guard & unique_lock β protecting shared data
- std::future, std::promise, std::async β getting results from threads
- Atomics β lock-free programming for simple types
Weβll go very slowly, with real-life analogies, tons of examples, common mistakes, and modern best practices (2026 style).
Letβs start!
1. std::thread β Creating Independent Threads
Include: #include <thread>
Basic example β two threads running in parallel
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> #include <thread> void task1() { for (int i = 0; i < 5; ++i) { std::cout << "Task 1: " << i << "\n"; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } void task2() { for (int i = 0; i < 5; ++i) { std::cout << "Task 2: " << i << "\n"; std::this_thread::sleep_for(std::chrono::milliseconds(300)); } } int main() { std::thread t1(task1); // starts task1 in a new thread std::thread t2(task2); // starts task2 in another thread std::cout << "Main thread is running...\n"; t1.join(); // wait for t1 to finish t2.join(); // wait for t2 to finish std::cout << "All threads finished!\n"; return 0; } |
Important methods:
- join() β wait for thread to finish (most common)
- detach() β let thread run independently (dangerous β no control)
- joinable() β check if thread can be joined
- std::this_thread::sleep_for() β pause current thread
Passing arguments to thread:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
void printSum(int a, int b, std::string message) { std::cout << message << ": " << (a + b) << "\n"; } int main() { std::thread t(printSum, 10, 20, "Sum is"); t.join(); } |
Modern best practice: Use lambda to capture variables safely:
|
0 1 2 3 4 5 6 7 8 9 10 |
int counter = 0; std::thread t([&counter]() { for (int i = 0; i < 1000000; ++i) ++counter; }); t.join(); |
2. Mutex β Protecting Shared Data (Avoiding Data Races)
Problem: Multiple threads accessing same variable β data race β undefined behavior
Solution: std::mutex + locking
Include: #include <mutex>
Classic example β Bank account with concurrent deposits
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> #include <thread> #include <mutex> class BankAccount { private: double balance = 1000.0; std::mutex mtx; // protects balance public: void deposit(double amount) { std::lock_guard<std::mutex> lock(mtx); // automatic lock & unlock! if (amount > 0) { balance += amount; std::cout << "Deposited " << amount << ". New balance: " << balance << "\n"; } } double getBalance() const { std::lock_guard<std::mutex> lock(mtx); return balance; } }; int main() { BankAccount acc; std::thread t1([&acc]() { acc.deposit(500); }); std::thread t2([&acc]() { acc.deposit(300); }); std::thread t3([&acc]() { acc.deposit(700); }); t1.join(); t2.join(); t3.join(); std::cout << "Final balance: " << acc.getBalance() << "\n"; return 0; } |
Two popular RAII locks:
| Lock Type | When to use | Features |
|---|---|---|
| std::lock_guard | Simple, automatic lock/unlock | Cannot unlock manually |
| std::unique_lock | More flexible (can unlock manually, moveable) | Can defer lock, try_lock, timed lock |
Example with unique_lock:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
void withdraw(double amount) { std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // don't lock yet if (amount <= balance) { lock.lock(); // lock now balance -= amount; } } |
3. std::future, std::promise, std::async β Getting Results from Threads
std::future β a handle to a value that will be available in the future std::promise β the other end β you set the value std::async β easiest way to run a function asynchronously
Simple example with std::async:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <future> #include <iostream> int computeSum(int n) { int sum = 0; for (int i = 1; i <= n; ++i) { sum += i; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return sum; } int main() { // Run computeSum in background std::future<int> fut = std::async(std::launch::async, computeSum, 100); std::cout << "Doing other work in main...\n"; int result = fut.get(); // wait and get result std::cout << "Sum = " << result << "\n"; return 0; } |
Using std::promise (more control):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void compute(int n, std::promise<int> prom) { int sum = 0; for (int i = 1; i <= n; ++i) sum += i; prom.set_value(sum); // send result } int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread t(compute, 100, std::move(prom)); std::cout << "Result: " << fut.get() << "\n"; t.join(); } |
4. Atomics β Lock-Free Operations for Simple Types
std::atomic<T> β thread-safe operations without mutex
Include: #include <atomic>
Example β Safe counter without mutex
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <atomic> #include <thread> std::atomic<int> counter(0); void increment() { for (int i = 0; i < 1000000; ++i) { ++counter; // atomic increment β safe! } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Final counter: " << counter << "\n"; // exactly 2000000 } |
Common atomic operations:
- load(), store()
- fetch_add(), fetch_sub()
- compare_exchange_strong() (CAS β very powerful)
When to use atomics:
- Simple counters, flags
- Lock-free data structures
Summary Table β Quick Reference
| Feature | Best Use Case | Key Class / Function |
|---|---|---|
| Creating threads | Run independent tasks | std::thread |
| Protect shared data | Avoid data races | std::mutex + lock_guard / unique_lock |
| Get result from thread | Async computation | std::future + std::async |
| Lock-free simple operations | Counters, flags | std::atomic |
Your Mini Homework (Try These!)
- Create two threads that print numbers 1 to 10, but synchronized so they print alternately.
- Write a thread-safe queue using std::mutex and std::condition_variable (advanced bonus).
- Use std::async to compute factorial of a large number in background.
- Make a global std::atomic<bool> ready flag and have one thread set it after work.
Youβre doing absolutely phenomenal! Youβve just mastered modern multithreading in C++ β this is the level needed for game engines, web servers, scientific computing, and almost every high-performance application.
Next lesson: File I/O, Streams & Serialization β reading/writing files, binary data, JSON/XML β super useful for real apps!
Any questions? Confused about lock_guard vs unique_lock? Want more examples with atomics or futures? Just ask β your friendly C++ teacher is right here for you! π
