Concurrency and Multithreading in C++: From Basics to Modern Standards
Modern applications increasingly require simultaneous execution of multiple tasks. Whether it's processing user requests on a server, rendering graphics, or working with big data — concurrency and multithreading in C++ have become essential developer skills. In this article, we will break down the key mechanisms available in the C++ standard library, from C++11 to C++20. You will learn how to create threads, synchronize their work, and avoid common mistakes.
If you are already familiar with the basics of C++ but want to delve deeper into parallelism — this material is for you. We will not delve into operating system theory, but will focus on practical tools: std::thread, std::mutex, std::async, and atomic operations.
1. Basics of Multithreading: std::thread and Thread Management
C++11 introduced standard thread support through the std::thread class. You can create a new thread by passing a callable object — a function, lambda, or functor — to the constructor.
#include <iostream>#include <thread>
void hello() { std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Creating a thread t.join(); // Waiting for the thread to finish return 0;}It is important to always call join() or detach() for each created thread, otherwise the program will terminate abnormally when the std::thread object is destroyed. join() blocks the current thread until the child thread completes, while detach() allows the thread to run independently.
To pass arguments to a thread, use additional constructor parameters:
#include <thread>#include <string>
void print_sum(int a, int b, const std::string& msg) { std::cout << msg << ": " << (a + b) << std::endl;}
int main() { std::thread t(print_sum, 3, 4, "Sum"); t.join(); return 0;}Note: arguments are copied into the thread. If you need to pass a reference, use std::ref().
2. Thread Synchronization: Mutexes and Locks
When working with shared data from different threads, a data race occurs. To avoid this, synchronization is used. The primary tool is std::mutex.
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int shared_counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++shared_counter; mtx.unlock(); }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Result: " << shared_counter << std::endl; return 0;}However, manually calling lock() and unlock() is error-prone: if an exception occurs between them, the mutex will remain locked. It is better to use the RAII wrapper std::lock_guard or std::unique_lock:
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++shared_counter; } // automatically unlocks when leaving scope}For more flexible control (e.g., conditional locking), use std::unique_lock. And if you need to synchronize multiple mutexes without deadlock, use std::lock().
3. Asynchronous Tasks: std::async and std::future
It is not always convenient to manage threads manually.