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
#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 concurrentlyWarning: 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
#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
#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
#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 locksstd::atomic — Lock-Free Operations
For simple types, std::atomic provides thread-safe read-modify-write operations without a mutex:
#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
#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:
#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
#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
#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::threadruns a function concurrently — always calljoin()ordetach()before the thread object is destroyed- Data races occur when multiple threads access shared data without synchronisation — undefined behaviour
std::mutex+lock_guardprovides mutual exclusion —lock_guard(RAII) releases the lock automaticallystd::atomic<T>provides lock-free thread-safe operations for simple types — use for counters and flagscondition_variableallows threads to wait for a condition efficiently — essential for producer-consumer patternsstd::async/std::futurelaunch tasks and retrieve results — the simplest way to parallelise independent workthread::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.