Multithreading: Concurrent Execution
C++11 introduced a standard threading library (<thread>), enabling portable concurrent programming without platform-specific APIs.
Creating Threads
#include <thread>
#include <iostream>
void task(int id) {
std::cout << "Thread " << id << " executing\\n";
}
int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
return 0;
}
Thread Operations
Join vs Detach
std::thread t(task);
// Option 1: Join (wait for completion)
t.join(); // Blocks until thread completes
// Option 2: Detach (independent execution)
t.detach(); // Thread runs independently
// Cannot join() after detach()
Critical: Every thread must be either joined or detached before destruction, otherwise std::terminate is called.
{
std::thread t(task);
} // CRASH: thread destructor called without join/detach
Thread Guard RAII
class thread_guard {
std::thread& t_;
public:
explicit thread_guard(std::thread& t) : t_(t) {}
~thread_guard() {
if (t_.joinable()) t_.join();
}
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
void func() {
std::thread t(task);
thread_guard g(t);
// Even if exception thrown, thread is joined
}
Passing Arguments
void task(int x, const std::string& s) {
std::cout << x << ": " << s << '\\n';
}
std::thread t(task, 42, "hello"); // Args passed by value
Reference Arguments
void modify(int& x) {
x = 100;
}
int value = 0;
std::thread t(modify, std::ref(value)); // Must use std::ref
t.join();
std::cout << value; // 100
Data Races and Undefined Behavior
Concurrent modification of shared data without synchronization causes data races (undefined behavior):
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // DATA RACE!
}
}
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter; // Unpredictable result (< 200000)
Mutexes: Mutual Exclusion
std::mutex provides mutual exclusion:
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
++counter;
mtx.unlock();
}
}
Problem: If exception thrown between lock/unlock, mutex stays locked (deadlock).
Lock Guards: RAII for Mutexes
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // Locks mutex
++counter;
} // Automatically unlocks when lock goes out of scope
}
unique_lock: Flexible Locking
More flexible than lock_guard:
std::mutex mtx;
void task() {
std::unique_lock<std::mutex> lock(mtx);
// Do work...
lock.unlock(); // Manually unlock
// Do work without lock...
lock.lock(); // Relock
// More work...
} // Automatically unlocks if locked
Deferred Locking
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // Don't lock yet
// ...
lock.lock(); // Lock when ready
Avoiding Deadlocks
Deadlock Example
std::mutex m1, m2;
void thread1() {
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2); // Potential deadlock
}
void thread2() {
std::lock_guard<std::mutex> lock2(m2);
std::lock_guard<std::mutex> lock1(m1); // Opposite order
}
Solution: std::lock
void thread1() {
std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
std::lock(lock1, lock2); // Atomically locks both, deadlock-free
}
C++17 std::scoped_lock simplifies this:
void thread1() {
std::scoped_lock lock(m1, m2); // Locks both, unlocks in destructor
}
Thread Identification
std::thread::id main_thread_id = std::this_thread::get_id();
void task() {
auto id = std::this_thread::get_id();
std::cout << "Thread ID: " << id << '\\n';
}
Hardware Concurrency
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware supports " << n << " concurrent threads\\n";
Conceptual Check
What happens if a thread object is destroyed without calling join() or detach()?
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <vector>
5
6std::mutex mtx;
7int counter = 0;
8
9void increment(int count) {
10 for (int i = 0; i < count; ++i) {
11 std::lock_guard<std::mutex> lock(mtx);
12 ++counter;
13 }
14}
15
16int main() {
17 const int iterations = 10000;
18 std::vector<std::thread> threads;
19
20 for (int i = 0; i < 4; ++i) {
21 threads.emplace_back(increment, iterations);
22 }
23
24 for (auto& t : threads) {
25 t.join();
26 }
27
28 std::cout << "Counter: " << counter << '\n';
29 std::cout << "Expected: " << (4 * iterations) << '\n';
30
31 return 0;
32}
System Console
Waiting for signal...