Introduction to Concurrency and Multithreading in C++
Modern processors have multiple cores, and to use their full power, programs must be able to execute several tasks simultaneously. Concurrency is the ability of a program to execute multiple tasks in overlapping time intervals, and multithreading is one way to implement it, where the program is divided into several threads running in parallel.
In C++, multithreading support appeared in the C++11 standard and has been actively evolving since then. In this article, we will cover all key aspects: from creating threads to complex synchronization patterns.
Basics: Creating and Managing Threads
std::thread — The Basic Building Block
The std::thread class allows you to run a function or callable object in a separate thread. Let's look at a simple example:
#include <iostream>#include <thread>
void hello() { std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Start the thread t.join(); // Wait for the thread to finish return 0;}The join() method makes the main thread wait for the child thread to complete. An alternative is detach(), which allows the thread to run independently but requires special caution (the thread may outlive the objects that created it).
Passing Arguments and Lambda Functions
You can pass arguments to threads and use lambdas:
#include <thread>#include <string>
void process(int id, const std::string& data) { // Data processing}
int main() { std::thread t1(process, 1, "data"); std::thread t2([](int x) { // Lambda expression }, 42); t1.join(); t2.join();}Thread Synchronization: Protecting Shared Data
When multiple threads access the same data, a race condition occurs. Synchronization mechanisms are used to prevent this.
Mutexes (std::mutex)
A mutex is a synchronization primitive that guarantees that only one thread can own it at any given time:
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++counter; // Protected access }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter: " << counter << std::endl; return 0;}std::lock_guard is an RAII wrapper that automatically locks the mutex upon creation and unlocks it upon destruction. This protects against accidentally forgetting to release the mutex.
Condition Variables (std::condition_variable)
Condition variables allow threads to wait for a specific event to occur:
#include <queue>#include <condition_variable>
std::queue<int> data_queue;std::mutex queue_mutex;std::condition_variable cv;
void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock(queue_mutex); data_queue.push(i); cv.notify_one(); // Notify the consumer }}
void consumer() { while (true) { std::unique_lock<std::mutex> lock(queue_mutex); cv.wait(lock, []{ return !data_queue.empty(); }); int value = data_queue.front(); data_queue.pop(); lock.unlock(); // Process