Module 28: Multithreading in C++
Introduction
Modern computers contain multiple CPU cores, allowing programs to execute multiple tasks simultaneously. C++11 introduced a powerful multithreading library in <thread> that enables developers to build fast, responsive, and scalable applications.
Multithreading is widely used in:
- Web servers
- Game engines
- AI and Machine Learning
- Operating Systems
- Video processing
- Databases
- Robotics
- Financial systems
This module covers the fundamentals of multithreading, synchronization, and thread-safe programming in Modern C++.
Learning Objectives
After completing this module, you will understand:
- What is multithreading?
- Creating threads with
std::thread - Thread lifecycle
- Joining and detaching threads
std::mutexstd::lock_guardstd::unique_lockstd::condition_variablestd::atomic- Race Condition
- Deadlock
- Best practices
What is Multithreading?
A thread is the smallest unit of execution within a process.
A single process can have multiple threads running concurrently.
Program
│
├── Thread 1
├── Thread 2
├── Thread 3
└── Thread 4
Each thread executes independently.
Why Use Multithreading?
Without threads:
Task 1
↓
Task 2
↓
Task 3
↓
Finished
With threads:
Task 1 ──────┐
Task 2 ──────┼── Run Together
Task 3 ──────┘
Better CPU utilization and faster execution.
Creating a Thread
Header
1#include <thread>
Example
1#include <iostream> 2#include <thread> 3 4using namespace std; 5 6void printMessage() 7{ 8 cout << "Hello from thread\n"; 9} 10 11int main() 12{ 13 thread t(printMessage); 14 15 t.join(); 16 17 return 0; 18}
Output
Hello from thread
Multiple Threads
1#include <iostream> 2#include <thread> 3 4using namespace std; 5 6void task(int id) 7{ 8 cout << "Thread " << id << " is running\n"; 9} 10 11int main() 12{ 13 thread t1(task, 1); 14 thread t2(task, 2); 15 thread t3(task, 3); 16 17 t1.join(); 18 t2.join(); 19 t3.join(); 20}
Possible Output
Thread 1 is running
Thread 3 is running
Thread 2 is running
Note: The execution order is not guaranteed.
join()
join() waits for a thread to finish.
1thread t(task); 2 3t.join();
Main Thread
↓
Wait
↓
Worker Thread Finishes
↓
Continue
detach()
Detached threads run independently.
1#include <iostream> 2#include <thread> 3#include <chrono> 4 5using namespace std; 6 7void work() 8{ 9 this_thread::sleep_for(chrono::seconds(2)); 10 cout << "Background Work Finished\n"; 11} 12 13int main() 14{ 15 thread t(work); 16 17 t.detach(); 18 19 cout << "Main Thread Finished\n"; 20}
Race Condition
A race condition occurs when multiple threads modify shared data simultaneously without synchronization.
Example
1#include <iostream> 2#include <thread> 3 4using namespace std; 5 6int counter = 0; 7 8void increment() 9{ 10 for(int i = 0; i < 100000; i++) 11 counter++; 12} 13 14int main() 15{ 16 thread t1(increment); 17 thread t2(increment); 18 19 t1.join(); 20 t2.join(); 21 22 cout << counter; 23}
Expected
200000
Actual output may vary due to concurrent updates.
mutex
A mutex (Mutual Exclusion) allows only one thread to access a critical section at a time.
1#include <mutex>
mutex Example
1#include <iostream> 2#include <thread> 3#include <mutex> 4 5using namespace std; 6 7int counter = 0; 8mutex mtx; 9 10void increment() 11{ 12 for(int i = 0; i < 100000; i++) 13 { 14 mtx.lock(); 15 16 counter++; 17 18 mtx.unlock(); 19 } 20} 21 22int main() 23{ 24 thread t1(increment); 25 thread t2(increment); 26 27 t1.join(); 28 t2.join(); 29 30 cout << counter; 31}
Output
200000
lock_guard
Instead of manually locking and unlocking, use lock_guard.
1#include <iostream> 2#include <thread> 3#include <mutex> 4 5using namespace std; 6 7mutex mtx; 8 9void print() 10{ 11 lock_guard<mutex> lock(mtx); 12 13 cout << "Protected Section\n"; 14} 15 16int main() 17{ 18 thread t1(print); 19 thread t2(print); 20 21 t1.join(); 22 t2.join(); 23}
Advantages:
- Automatic unlock
- Exception safe
- Simple to use
unique_lock
unique_lock provides more flexibility than lock_guard.
Features:
- Can lock later
- Can unlock early
- Can transfer ownership
- Required by
condition_variable
Example
1#include <iostream> 2#include <thread> 3#include <mutex> 4 5using namespace std; 6 7mutex mtx; 8 9void work() 10{ 11 unique_lock<mutex> lock(mtx); 12 13 cout << "Working...\n"; 14 15 lock.unlock(); 16 17 cout << "Lock Released\n"; 18} 19 20int main() 21{ 22 thread t(work); 23 24 t.join(); 25}
Output
Working...
Lock Released
Condition Variable
A condition variable allows one thread to wait until another thread notifies it.
Headers
1#include <condition_variable> 2#include <mutex>
Example
1#include <iostream> 2#include <thread> 3#include <mutex> 4#include <condition_variable> 5 6using namespace std; 7 8mutex mtx; 9condition_variable cv; 10bool ready = false; 11 12void worker() 13{ 14 unique_lock<mutex> lock(mtx); 15 16 cv.wait(lock, []{ return ready; }); 17 18 cout << "Worker Started\n"; 19} 20 21int main() 22{ 23 thread t(worker); 24 25 { 26 lock_guard<mutex> lock(mtx); 27 ready = true; 28 } 29 30 cv.notify_one(); 31 32 t.join(); 33}
Output
Worker Started
notify_all()
Wake every waiting thread.
1cv.notify_all();
notify_one()
Wake only one waiting thread.
1cv.notify_one();
Atomic Variables
Atomic operations occur without interruption.
1#include <atomic>
Atomic Example
1#include <iostream> 2#include <thread> 3#include <atomic> 4 5using namespace std; 6 7atomic<int> counter = 0; 8 9void increment() 10{ 11 for(int i = 0; i < 100000; i++) 12 counter++; 13} 14 15int main() 16{ 17 thread t1(increment); 18 thread t2(increment); 19 20 t1.join(); 21 t2.join(); 22 23 cout << counter; 24}
Output
200000
No mutex is needed for this simple increment.
Race Condition vs Atomic
Without Atomic
Read Counter
↓
Increment
↓
Write Counter
Two threads may overwrite each other's updates.
With Atomic
Atomic Increment
↓
Single Safe Operation
Deadlock
A deadlock happens when two or more threads wait forever for each other to release resources.
Deadlock Example
1#include <iostream> 2#include <thread> 3#include <mutex> 4 5using namespace std; 6 7mutex m1; 8mutex m2; 9 10void task1() 11{ 12 lock_guard<mutex> lock1(m1); 13 14 this_thread::sleep_for(chrono::milliseconds(100)); 15 16 lock_guard<mutex> lock2(m2); 17} 18 19void task2() 20{ 21 lock_guard<mutex> lock1(m2); 22 23 this_thread::sleep_for(chrono::milliseconds(100)); 24 25 lock_guard<mutex> lock2(m1); 26} 27 28int main() 29{ 30 thread t1(task1); 31 thread t2(task2); 32 33 t1.join(); 34 t2.join(); 35}
This program may deadlock because each thread waits for a mutex held by the other.
Avoiding Deadlocks
Use std::scoped_lock (C++17) to lock multiple mutexes safely.
1#include <iostream> 2#include <thread> 3#include <mutex> 4 5using namespace std; 6 7mutex m1; 8mutex m2; 9 10void task() 11{ 12 scoped_lock lock(m1, m2); 13 14 cout << "Safe Locking\n"; 15} 16 17int main() 18{ 19 thread t1(task); 20 thread t2(task); 21 22 t1.join(); 23 t2.join(); 24}
Thread Synchronization
Thread 1
↓
Lock Mutex
↓
Critical Section
↓
Unlock
↓
Thread 2
Only one thread accesses shared data at a time.
Producer-Consumer Example
1#include <iostream> 2#include <queue> 3#include <thread> 4#include <mutex> 5#include <condition_variable> 6 7using namespace std; 8 9queue<int> buffer; 10mutex mtx; 11condition_variable cv; 12 13void producer() 14{ 15 { 16 lock_guard<mutex> lock(mtx); 17 buffer.push(100); 18 } 19 20 cv.notify_one(); 21} 22 23void consumer() 24{ 25 unique_lock<mutex> lock(mtx); 26 27 cv.wait(lock, [] 28 { 29 return !buffer.empty(); 30 }); 31 32 cout << buffer.front() << endl; 33 34 buffer.pop(); 35} 36 37int main() 38{ 39 thread t1(producer); 40 thread t2(consumer); 41 42 t1.join(); 43 t2.join(); 44}
Output
100
Real-World Applications
| Application | Multithreading Use |
|---|---|
| Web Server | Handle multiple client requests |
| Browser | Render UI and download files simultaneously |
| AI Training | Parallel tensor computations |
| Video Editor | Encode multiple frames concurrently |
| Database | Serve multiple queries at the same time |
| Game Engine | Physics, rendering, audio, and AI on separate threads |
Best Practices
✅ Always join() or detach() every thread.
✅ Protect shared data using mutex.
✅ Prefer lock_guard for simple locking.
✅ Use unique_lock when flexible locking is required.
✅ Use condition_variable for thread communication.
✅ Use std::atomic for simple counters and flags.
✅ Keep critical sections as short as possible.
✅ Lock multiple mutexes in a consistent order or use std::scoped_lock.
Common Mistakes
Forgetting to Join
1thread t(task); 2 3// Missing t.join()
This causes std::terminate() when the thread object is destroyed.
Manual Lock Without Unlock
Wrong
1mtx.lock(); 2 3// exception occurs 4 5// unlock never called
Correct
1lock_guard<mutex> lock(mtx);
Sharing Data Without Protection
1counter++;
If multiple threads access the same variable, use a mutex or an atomic type.
Interview Questions
1. What is multithreading?
Executing multiple threads concurrently within the same process.
2. What is std::thread?
A C++ class used to create and manage threads.
3. What is a mutex?
A synchronization primitive that allows only one thread to access a critical section at a time.
4. Difference between lock_guard and unique_lock?
| lock_guard | unique_lock |
|---|---|
| Lightweight | More flexible |
| Auto-locks immediately | Can defer locking |
| Cannot unlock manually | Can unlock and relock |
| Lower overhead | Slightly higher overhead |
5. What is a race condition?
When multiple threads access shared data simultaneously without proper synchronization, leading to unpredictable results.
6. What is a deadlock?
A situation where two or more threads wait indefinitely for resources held by each other.
7. When should std::atomic be used?
For simple thread-safe operations such as counters, flags, or increment/decrement operations without requiring a mutex.
8. What is a condition variable?
A synchronization mechanism that allows threads to wait efficiently until a specific condition becomes true.
Module Summary
In this module, you learned:
- How to create and manage threads using
std::thread - The purpose of
join()anddetach() - Protecting shared resources with
std::mutex - Automatic locking using
std::lock_guard - Flexible locking using
std::unique_lock - Thread communication with
std::condition_variable - Lock-free synchronization using
std::atomic - Understanding and preventing race conditions
- Understanding and avoiding deadlocks
- Best practices for writing safe, efficient, and scalable multithreaded C++ applications