Concurrency and Multithreading in C++: Modern Approaches and Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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.

Blogs

Book Recommendations