Concurrency and Multithreading in C++: From Basics to Advanced Techniques
Modern applications increasingly demand maximum performance, especially when working with multi-core processors. C++ offers powerful tools for working with concurrency and multithreading, starting from C++11 and up to the latest standards. In this article, we will cover advanced aspects of multithreading: from proper use of mutexes and atomic operations to complex synchronization patterns. The material is intended for developers already familiar with the basics of threads in C++.
1. Thread Management: std::thread and std::jthread
The std::thread class is the foundation of multithreaded programming in C++. However, at an advanced level, it is important not only to create a thread but also to properly manage its lifecycle. C++20 introduced std::jthread, which automatically joins upon destruction, preventing resource leaks.
Example of using std::jthread:
#include <iostream>#include <thread>#include <chrono>
void worker(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout << "Thread " << id << " completed";}
int main() { // std::jthread automatically joins when leaving scope std::jthread t1(worker, 1); std::jthread t2(worker, 2); std::jthread t3(worker, 3); // No need to explicitly call join() or detach() // When t1, t2, t3 are destroyed, main waits for their completion std::cout << "Main thread continues working"; return 0;}It is important to remember: std::jthread also supports a stop mechanism via std::stop_token, which allows safely terminating long-running background tasks.
2. Synchronizing Access: Mutexes, Locks, and Condition Variables
The main problem of multithreading is data races. Mutexes are used to prevent them. At an advanced level, it is recommended to use std::scoped_lock (C++17) to lock multiple mutexes without the risk of deadlock.
Example with std::scoped_lock:
#include <iostream>#include <mutex>#include <thread>#include <vector>
std::mutex mtx1, mtx2;int shared_data = 0;
void safe_increment(int id) { // scoped_lock locks both mutexes in order, avoiding deadlock std::scoped_lock lock(mtx1, mtx2); ++shared_data; std::cout << "Thread " << id << " incremented value to " << shared_data << "";}
int main() { std::vector<std::jthread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(safe_increment, i); } return 0;}For more complex synchronization scenarios (e.g., producer-consumer), condition variables (std::condition_variable) are used. They allow threads to wait for a specific event without wasting resources on busy waiting.
3. Atomic Operations and the C++ Memory Model
Atomic operations (std::atomic) are the foundation of lock-free programming. They guarantee that reading and writing a variable are performed as a single indivisible action. However, at an advanced level, it is important to understand the C++ memory model and memory ordering levels.
Main memory ordering levels:
memory_order_relaxed— minimal guarantees, only atomicitymemory_order_acquire— guarantees that all subsequent reads and writes will see the results of previous operationsmemory_order_release— guarantees that all previous operations will be visible after this writememory_order_acq_rel— combination of acquire and release