15

Module 15: Concurrency and Multithreading

Chapter 15 • Advanced

60 min

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

cpp.js
#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

cpp.js
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

cpp.js
thread t([]() {
    cout << "Lambda thread" << endl;
});
t.join();

Thread Management

join() vs detach()

cpp.js
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:

cpp.js
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

cpp.js
#include <mutex>

mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; i++) {
        mtx.lock();
        counter++;
        mtx.unlock();
    }
}

lock_guard (RAII)

cpp.js
void increment() {
    for (int i = 0; i < 100000; i++) {
        lock_guard<mutex> lock(mtx);  // Automatic unlock
        counter++;
    }
}

unique_lock

cpp.js
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.

cpp.js
timed_mutex mtx;
if (mtx.try_lock_for(chrono::seconds(1))) {
    // Lock acquired
    mtx.unlock();
}

Condition Variables

Allow threads to wait for conditions:

cpp.js
#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:

cpp.js
#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:

cpp.js
#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:

cpp.js
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:

cpp.js
auto future = async(launch::async, compute);
int result = future.get();

Thread Safety

Thread-Safe Guidelines

  1. Use mutexes for shared mutable data
  2. Use atomic for simple operations
  3. Avoid shared state when possible
  4. Use lock_guard for automatic unlocking
  5. Minimize lock scope (lock late, unlock early)
  6. Avoid deadlocks (lock in consistent order)
  7. Use condition variables for coordination
  8. Prefer immutable data structures

Common Patterns

Pattern 1: Producer-Consumer

cpp.js
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

cpp.js
// Use thread pool library or implement
// Executes tasks in parallel

Deadlocks

Deadlock occurs when threads wait for each other indefinitely.

Avoiding Deadlocks

  1. Lock in consistent order
  2. Use timeout locks
  3. Avoid nested locks
  4. Use lock_guard (automatic unlock)

Best Practices

  1. Minimize shared state
  2. Use RAII for locks (lock_guard)
  3. Prefer atomic for simple operations
  4. Use condition variables for coordination
  5. Avoid data races
  6. Test thoroughly (race conditions are hard to debug)
  7. Use thread-safe containers when available
  8. 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.