Imagine a restaurant kitchen. With one chef (single thread), orders are prepared sequentially. But with multiple chefs (multiple threads), several dishes can be prepared simultaneously. That's the power of multithreading!
#include <iostream>
#include <thread> // Required for std::thread
// Function to be executed by a thread
void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
std::cout << "Thread " << std::this_thread::get_id()
<< ": " << i << std::endl;
}
}
int main() {
// Create two threads
std::thread t1(printNumbers, 1, 5); // Thread 1: prints 1-5
std::thread t2(printNumbers, 10, 15); // Thread 2: prints 10-15
// Wait for both threads to complete
t1.join(); // Main thread waits for t1
t2.join(); // Main thread waits for t2
std::cout << "All threads completed!" << std::endl;
return 0;
}
// join() - Wait for thread to finish
std::thread worker(doWork);
worker.join(); // Main thread blocks here until worker finishes
// Safe to continue - worker is done
// detach() - Let thread run independently
std::thread background(backgroundTask);
background.detach(); // Thread continues on its own
// Main can continue immediately - background still running
⚠️ Important Rule: You MUST either join() or detach() a thread before its destructor is called. Failing to do so will terminate your program!
When multiple threads access shared data simultaneously, chaos can ensue. It's like two people trying to edit the same document at the same time without coordination.
#include <iostream>
#include <thread>
#include <vector>
int counter = 0; // Shared variable - DANGER!
void incrementCounter(int iterations) {
for (int i = 0; i < iterations; i++) {
counter++; // NOT thread-safe!
}
}
int main() {
const int num_threads = 4;
const int iterations = 100000;
std::vector<std::thread> threads;
// Create threads
for (int i = 0; i < num_threads; i++) {
threads.emplace_back(incrementCounter, iterations);
}
// Wait for all threads
for (auto& t : threads) {
t.join();
}
std::cout << "Expected: " << num_threads * iterations << std::endl;
std::cout << "Actual: " << counter << std::endl;
// Result is unpredictable! Often less than expected.
return 0;
}
A mutex (mutual exclusion) is like a bathroom with a lock. Only one person (thread) can use it at a time. Others must wait their turn.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
int counter = 0;
std::mutex counter_mutex; // Protects counter
void safeIncrementCounter(int iterations) {
for (int i = 0; i < iterations; i++) {
// Lock the mutex before accessing shared data
counter_mutex.lock();
counter++; // Now thread-safe!
counter_mutex.unlock();
}
}
// Better approach using lock_guard (RAII)
void betterIncrementCounter(int iterations) {
for (int i = 0; i < iterations; i++) {
std::lock_guard<std::mutex> lock(counter_mutex);
counter++; // Automatically unlocks when lock goes out of scope
} // lock_guard destructor unlocks mutex here
}
void updateSharedData() {
std::lock_guard<std::mutex> lock(data_mutex);
// Mutex is locked
shared_data.modify();
// Mutex automatically unlocks when lock goes out of scope
// Even if an exception is thrown!
}
void flexibleOperation() {
std::unique_lock<std::mutex> lock(data_mutex);
// Can manually unlock and relock
process_part1();
lock.unlock(); // Release lock temporarily
do_something_else(); // Other threads can access data
lock.lock(); // Reacquire lock
process_part2();
}
std::mutex m;
std::condition_variable cv;
bool ready = false;
// Producer thread
void producer() {
std::unique_lock<std::mutex> lock(m);
prepare_data();
ready = true;
cv.notify_one(); // Wake up waiting thread
}
// Consumer thread
void consumer() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, []{ return ready; }); // Wait until ready
process_data();
}
Deadlock occurs when threads wait for each other indefinitely. Imagine two people who need both a pen and paper to work, but each grabbed one item and won't let go until they get the other.
// Problem: Potential deadlock
void thread1() {
lock1.lock();
lock2.lock(); // If thread2 has lock2, DEADLOCK!
// work...
}
void thread2() {
lock2.lock();
lock1.lock(); // If thread1 has lock1, DEADLOCK!
// work...
}
// Solution 1: Always lock in the same order
void thread1_safe() {
lock1.lock();
lock2.lock();
// work...
}
void thread2_safe() {
lock1.lock(); // Same order as thread1
lock2.lock();
// work...
}
// Solution 2: Use std::lock to lock multiple mutexes
void thread_safest() {
std::lock(mutex1, mutex2); // Locks both atomically
std::lock_guard<std::mutex> lk1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lk2(mutex2, std::adopt_lock);
// work...
}
Atomic operations are like using a vending machine - the entire transaction (insert money, press button, receive item) happens as one indivisible operation.
#include <atomic>
std::atomic<int> counter{0}; // Atomic integer
void atomicIncrement(int iterations) {
for (int i = 0; i < iterations; i++) {
counter++; // Thread-safe without mutex!
}
}
// More atomic operations
std::atomic<bool> ready{false};
void producer() {
prepare_data();
ready.store(true); // Atomic write
}
void consumer() {
while (!ready.load()) { // Atomic read
// Wait...
}
process_data();
}
Instead of creating threads for each task, maintain a pool of worker threads. It's like having permanent employees instead of hiring contractors for each job.
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <condition_variable>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t threads) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
cv.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
};
Process multiple files concurrently:
void processFile(const std::string& filename) {
// Simulate file processing
std::cout << "Processing " << filename
<< " on thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
int main() {
std::vector<std::string> files = {"data1.txt", "data2.txt", "data3.txt"};
std::vector<std::thread> threads;
// Process each file in a separate thread
for (const auto& file : files) {
threads.emplace_back(processFile, file);
}
// Wait for all to complete
for (auto& t : threads) {
t.join();
}
return 0;
}
Implement a thread-safe queue where one thread produces data and another consumes it:
template<typename T>
class ThreadSafeQueue {
private:
std::queue<T> queue;
mutable std::mutex mutex;
std::condition_variable cv;
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mutex);
queue.push(std::move(value));
cv.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [this] { return !queue.empty(); });
T value = std::move(queue.front());
queue.pop();
return value;
}
};
You've learned the fundamentals of multithreading in C++:
Multithreading is like conducting an orchestra - it requires careful coordination, but when done right, the performance is magnificent. In the next lesson, we'll explore move semantics and rvalue references, which are crucial for efficient resource management in modern C++.