Module 15: Concurrency and Multithreading
Chapter 15 • Advanced
Concurrency and Multithreading
Concurrency allows programs to execute multiple operations simultaneously, improving performance and responsiveness. Modern C++ (C++11+) provides excellent support for multithreading.
What is Concurrency?
Concurrency is the ability of a program to execute multiple tasks simultaneously. In C++11+, this is achieved through:
- Threads: Separate execution paths
- Mutexes: Synchronization primitives
- Condition Variables: Thread coordination
- Futures/Promises: Asynchronous operations
Creating Threads
Basic Thread Creation
#include <thread>
#include <iostream>
using namespace std;
void threadFunction() {
cout << "Thread is running" << endl;
}
int main() {
thread t(threadFunction); // Create thread
t.join(); // Wait for thread to finish
return 0;
}
Thread with Arguments
void printMessage(const string& msg, int count) {
for (int i = 0; i < count; i++) {
cout << msg << " " << i << endl;
}
}
int main() {
thread t(printMessage, "Hello", 5);
t.join();
return 0;
}
Lambda Threads
thread t([]() {
cout << "Lambda thread" << endl;
});
t.join();
Thread Management
join() vs detach()
thread t(function);
t.join(); // Wait for thread to complete
// OR
t.detach(); // Let thread run independently (daemon)
Important: Must call either join() or detach() before thread object is destroyed!
Race Conditions
Multiple threads accessing shared data without synchronization causes race conditions:
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // Race condition!
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << counter << endl; // May not be 200000!
}
Mutex (Mutual Exclusion)
Mutex ensures only one thread accesses shared data at a time.
Basic Mutex
#include <mutex>
mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
mtx.lock();
counter++;
mtx.unlock();
}
}
lock_guard (RAII)
void increment() {
for (int i = 0; i < 100000; i++) {
lock_guard<mutex> lock(mtx); // Automatic unlock
counter++;
}
}
unique_lock
unique_lock<mutex> lock(mtx);
// Can unlock manually
lock.unlock();
// Can lock again
lock.lock();
Types of Mutexes
std::mutex
Basic mutex (non-recursive).
std::recursive_mutex
Allows same thread to lock multiple times.
std::timed_mutex
Mutex with timeout support.
timed_mutex mtx;
if (mtx.try_lock_for(chrono::seconds(1))) {
// Lock acquired
mtx.unlock();
}
Condition Variables
Allow threads to wait for conditions:
#include <condition_variable>
condition_variable cv;
mutex mtx;
bool ready = false;
void worker() {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // Wait for condition
// Do work
}
void producer() {
{
lock_guard<mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // Notify waiting thread
}
Atomic Operations
Atomic operations are thread-safe without mutexes:
#include <atomic>
atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // Thread-safe!
}
}
Benefits:
- No mutex overhead
- Lock-free operations
- Better performance for simple operations
Futures and Promises
std::future
Represents a value that will be available in the future:
#include <future>
int compute() {
return 42;
}
int main() {
future<int> f = async(compute);
int result = f.get(); // Wait for result
return 0;
}
std::promise
Set value from one thread, get from another:
promise<int> p;
future<int> f = p.get_future();
thread t([&p]() {
p.set_value(42);
});
int result = f.get(); // Wait for value
t.join();
std::async
Execute function asynchronously:
auto future = async(launch::async, compute);
int result = future.get();
Thread Safety
Thread-Safe Guidelines
- ✅ Use mutexes for shared mutable data
- ✅ Use atomic for simple operations
- ✅ Avoid shared state when possible
- ✅ Use lock_guard for automatic unlocking
- ✅ Minimize lock scope (lock late, unlock early)
- ✅ Avoid deadlocks (lock in consistent order)
- ✅ Use condition variables for coordination
- ✅ Prefer immutable data structures
Common Patterns
Pattern 1: Producer-Consumer
queue<int> dataQueue;
mutex mtx;
condition_variable cv;
void producer() {
for (int i = 0; i < 10; i++) {
{
lock_guard<mutex> lock(mtx);
dataQueue.push(i);
}
cv.notify_one();
}
}
void consumer() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, []{ return !dataQueue.empty(); });
int value = dataQueue.front();
dataQueue.pop();
lock.unlock();
// Process value
}
}
Pattern 2: Thread Pool
// Use thread pool library or implement
// Executes tasks in parallel
Deadlocks
Deadlock occurs when threads wait for each other indefinitely.
Avoiding Deadlocks
- Lock in consistent order
- Use timeout locks
- Avoid nested locks
- Use lock_guard (automatic unlock)
Best Practices
- ✅ Minimize shared state
- ✅ Use RAII for locks (lock_guard)
- ✅ Prefer atomic for simple operations
- ✅ Use condition variables for coordination
- ✅ Avoid data races
- ✅ Test thoroughly (race conditions are hard to debug)
- ✅ Use thread-safe containers when available
- ✅ Document thread safety guarantees
Common Mistakes
- ❌ Forgetting to join() or detach() threads
- ❌ Data races (unsynchronized access)
- ❌ Deadlocks (circular waiting)
- ❌ Using non-thread-safe functions
- ❌ Holding locks too long
- ❌ Not handling exceptions in threads
- ❌ Race conditions in initialization
Next Steps
You've completed the C++ course! Continue learning:
- Advanced Templates: Template metaprogramming
- Design Patterns: Reusable solutions
- C++20 Features: Concepts, coroutines, ranges
- Performance Optimization: Profiling, optimization techniques
- System Programming: OS APIs, low-level programming
Congratulations on mastering C++! 🎉
Hands-on Examples
Basic Threads
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void printMessage(const string& msg, int id) {
for (int i = 0; i < 3; i++) {
cout << "Thread " << id << ": " << msg << " " << i << endl;
this_thread::sleep_for(chrono::milliseconds(100));
}
}
void workerFunction(int id) {
cout << "Worker " << id << " started" << endl;
this_thread::sleep_for(chrono::milliseconds(500));
cout << "Worker " << id << " finished" << endl;
}
int main() {
cout << "=== Basic Thread Creation ===" << endl;
// Create thread with function
thread t1(printMessage, "Hello", 1);
thread t2(printMessage, "World", 2);
// Wait for threads to complete
t1.join();
t2.join();
cout << "\n=== Multiple Threads ===" << endl;
vector<thread> threads;
// Create multiple threads
for (int i = 0; i < 5; i++) {
threads.emplace_back(workerFunction, i);
}
// Wait for all threads
for (auto& t : threads) {
t.join();
}
cout << "\n=== Lambda Thread ===" << endl;
thread lambdaThread([]() {
cout << "Lambda thread executing" << endl;
this_thread::sleep_for(chrono::milliseconds(200));
cout << "Lambda thread done" << endl;
});
lambdaThread.join();
cout << "\nAll threads completed!" << endl;
return 0;
}Basic thread creation uses std::thread. Pass function and arguments to constructor. Must call join() to wait for completion or detach() to run independently. Threads execute concurrently. Use this_thread::sleep_for() for delays. Always join() or detach() before thread object is destroyed.
Practice with Programs
Reinforce your learning with hands-on practice programs