Advanced Topics

Multithreading

Master C++ multithreading — std::thread, mutex, lock_guard, condition_variable, atomic, async/future, thread pool pattern, and avoiding data races.

Multithreading

C++11 introduced a portable threading model in <thread>, <mutex>, <atomic>, and <future>. Multithreading allows your program to run multiple tasks concurrently — essential for CPU-intensive computation, I/O overlap, and responsive applications. This power comes with responsibility: data races cause undefined behaviour and are among the hardest bugs to debug.


Creating Threads

cpp
#include <iostream>
#include <thread>
#include <string>
using namespace std;

void greet(const string& name, int times) {
    for (int i = 0; i < times; i++) {
        cout << "Hello from " << name << "!\n";
    }
}

int main() {
    // Create threads — execution starts immediately
    thread t1(greet, "Alice", 3);
    thread t2(greet, "Bob",   3);

    // Lambda thread
    thread t3([]() {
        for (int i = 0; i < 3; i++) cout << "Lambda thread\n";
    });

    // join() — wait for thread to finish before continuing
    t1.join();
    t2.join();
    t3.join();

    cout << "All threads done\n";
    return 0;
}
// Output order is non-deterministic — threads run concurrently

Warning: If a thread is still running when the std::thread object is destroyed (without join or detach), std::terminate() is called. Always join() or detach() every thread.


Data Race — The Core Danger

cpp
#include <iostream>
#include <thread>
using namespace std;

int counter = 0;   // Shared variable

void increment(int n) {
    for (int i = 0; i < n; i++) {
        counter++;   // DATA RACE — not thread-safe!
        // Read-modify-write is NOT atomic; multiple threads corrupt the value
    }
}

int main() {
    thread t1(increment, 100000);
    thread t2(increment, 100000);
    t1.join(); t2.join();

    // Expected: 200000. Actual: ANYTHING — undefined behaviour!
    cout << "Counter: " << counter << "\n";
    return 0;
}

The fix is to synchronise access to shared data — either with a mutex or an atomic variable.


std::mutex and lock_guard

cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int    counter = 0;
mutex  mtx;         // Mutual exclusion lock

void safeIncrement(int n) {
    for (int i = 0; i < n; i++) {
        lock_guard<mutex> lock(mtx);   // Acquires mutex on construction
        counter++;                      // Protected — only one thread at a time
    }                                   // lock_guard destroyed — mutex released
}

int main() {
    thread t1(safeIncrement, 100000);
    thread t2(safeIncrement, 100000);
    t1.join(); t2.join();

    cout << "Counter: " << counter << "\n";   // Always 200000
    return 0;
}

lock_guard is RAII for mutexes — the lock is always released when the guard goes out of scope, even if an exception is thrown.


unique_lock — More Flexible

cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

void worker(int id) {
    unique_lock<mutex> lock(mtx);   // Acquires immediately (like lock_guard)

    cout << "Thread " << id << " acquired lock\n";

    lock.unlock();   // Can release early
    // Do non-critical work here
    lock.lock();     // Reacquire

    cout << "Thread " << id << " done\n";
}
// unique_lock supports: defer_lock, try_lock, timed locks

std::atomic — Lock-Free Operations

For simple types, std::atomic provides thread-safe read-modify-write operations without a mutex:

cpp
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<int>  counter{0};
atomic<bool> running{true};

void incrementAtomic(int n) {
    for (int i = 0; i < n; i++) {
        counter.fetch_add(1);   // Atomic increment — no mutex needed!
        // Or: counter++;       // Also atomic for std::atomic
    }
}

int main() {
    thread t1(incrementAtomic, 100000);
    thread t2(incrementAtomic, 100000);
    t1.join(); t2.join();

    cout << "Counter: " << counter.load() << "\n";   // Always 200000

    // Atomic bool — safe flag for signaling threads
    thread worker([&]() {
        while (running.load()) {
            // Do work
        }
        cout << "Worker stopped\n";
    });

    running.store(false);   // Signal worker to stop
    worker.join();

    return 0;
}

condition_variable — Thread Coordination

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;

mutex              mtx;
condition_variable cv;
queue<int>         workQueue;
bool               done = false;

void producer() {
    for (int i = 1; i <= 5; i++) {
        {
            lock_guard<mutex> lock(mtx);
            workQueue.push(i);
            cout << "Produced: " << i << "\n";
        }
        cv.notify_one();   // Wake one waiting thread
        this_thread::sleep_for(chrono::milliseconds(100));
    }
    {
        lock_guard<mutex> lock(mtx);
        done = true;
    }
    cv.notify_all();
}

void consumer(int id) {
    while (true) {
        unique_lock<mutex> lock(mtx);

        // Wait until there's work or we're done
        cv.wait(lock, []{ return !workQueue.empty() || done; });

        if (workQueue.empty() && done) break;

        int item = workQueue.front();
        workQueue.pop();
        lock.unlock();   // Release before processing

        cout << "Consumer " << id << " processed: " << item << "\n";
    }
}

int main() {
    thread p(producer);
    thread c1(consumer, 1);
    thread c2(consumer, 2);

    p.join(); c1.join(); c2.join();
    return 0;
}

std::async and std::future

async launches a task (on a thread pool or new thread) and returns a future to retrieve the result:

cpp
#include <iostream>
#include <future>
#include <numeric>
#include <vector>
using namespace std;

long long sumRange(int start, int end) {
    long long total = 0;
    for (int i = start; i <= end; i++) total += i;
    return total;
}

int main() {
    // Launch three tasks in parallel
    auto f1 = async(launch::async, sumRange, 1,       25000000);
    auto f2 = async(launch::async, sumRange, 25000001, 50000000);
    auto f3 = async(launch::async, sumRange, 50000001, 75000000);

    // Wait for each result (get() blocks until ready)
    long long result = f1.get() + f2.get() + f3.get();
    cout << "Sum(1..75M): " << result << "\n";
    // 2812500037500000

    // async for exception handling
    auto bad = async(launch::async, []() -> int {
        throw runtime_error("Task failed!");
        return 0;
    });

    try {
        int res = bad.get();   // Exception is rethrown here
    } catch (const runtime_error& e) {
        cerr << "Caught: " << e.what() << "\n";
    }

    return 0;
}

Thread Pool Pattern

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>
using namespace std;

class ThreadPool {
    vector<thread>          workers;
    queue<function<void()>> tasks;
    mutex                   mtx;
    condition_variable      cv;
    bool                    stop;

public:
    ThreadPool(size_t numThreads) : stop(false) {
        for (size_t i = 0; i < numThreads; i++) {
            workers.emplace_back([this]() {
                while (true) {
                    function<void()> task;
                    {
                        unique_lock<mutex> lock(mtx);
                        cv.wait(lock, [this]{ return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    void enqueue(function<void()> task) {
        {
            lock_guard<mutex> lock(mtx);
            tasks.push(move(task));
        }
        cv.notify_one();
    }

    ~ThreadPool() {
        {
            lock_guard<mutex> lock(mtx);
            stop = true;
        }
        cv.notify_all();
        for (auto& w : workers) w.join();
    }
};

int main() {
    ThreadPool pool(4);   // 4 worker threads
    mutex printMtx;

    for (int i = 1; i <= 10; i++) {
        pool.enqueue([i, &printMtx]() {
            // Simulate work
            this_thread::sleep_for(chrono::milliseconds(10 * i % 50));
            lock_guard<mutex> lock(printMtx);
            cout << "Task " << i << " completed by thread "
                 << this_thread::get_id() << "\n";
        });
    }

    this_thread::sleep_for(chrono::milliseconds(1000));
    // Pool destructor joins all threads

    return 0;
}

Practical Example: Parallel Image Processing

cpp
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
using namespace std;

struct Pixel { uint8_t r, g, b; };

using Image = vector<vector<Pixel>>;

// Process a range of rows (thread function)
void grayscaleRows(Image& img, int startRow, int endRow) {
    for (int r = startRow; r < endRow; r++) {
        for (auto& px : img[r]) {
            uint8_t gray = static_cast<uint8_t>(
                0.299 * px.r + 0.587 * px.g + 0.114 * px.b
            );
            px = {gray, gray, gray};
        }
    }
}

void parallelGrayscale(Image& img, int numThreads) {
    int    rows      = img.size();
    int    chunkSize = (rows + numThreads - 1) / numThreads;
    vector<thread> threads;

    for (int t = 0; t < numThreads; t++) {
        int start = t * chunkSize;
        int end   = min(start + chunkSize, rows);
        if (start < rows) {
            threads.emplace_back(grayscaleRows, ref(img), start, end);
        }
    }

    for (auto& t : threads) t.join();
}

int main() {
    // Create a 1000×1000 colour image
    Image img(1000, vector<Pixel>(1000, {255, 128, 0}));  // Orange

    auto start = chrono::high_resolution_clock::now();
    parallelGrayscale(img, thread::hardware_concurrency());
    auto end = chrono::high_resolution_clock::now();

    auto ms = chrono::duration_cast<chrono::milliseconds>(end - start).count();
    cout << "Processed 1M pixels in " << ms << "ms\n";
    cout << "First pixel gray value: " << (int)img[0][0].r << "\n";  // ~153

    return 0;
}

Summary

  • std::thread runs a function concurrently — always call join() or detach() before the thread object is destroyed
  • Data races occur when multiple threads access shared data without synchronisation — undefined behaviour
  • std::mutex + lock_guard provides mutual exclusion — lock_guard (RAII) releases the lock automatically
  • std::atomic<T> provides lock-free thread-safe operations for simple types — use for counters and flags
  • condition_variable allows threads to wait for a condition efficiently — essential for producer-consumer patterns
  • std::async / std::future launch tasks and retrieve results — the simplest way to parallelise independent work
  • thread::hardware_concurrency() returns the number of logical CPU cores — use as the default thread count
  • A thread pool amortises thread creation cost — create workers once, submit tasks repeatedly

Congratulations — you have completed the C++ Programming course!

You now have the skills to build everything from simple command-line programs to high-performance systems, games, and embedded software. The best path forward is building real projects: implement a data structure from scratch, contribute to an open-source C++ project, or solve competitive programming problems to sharpen your algorithmic thinking.