Introduction to Concurrency and Multithreading in C++
Modern applications demand maximum performance. Concurrency and multithreading in C++ allow efficient use of multi-core processors, speeding up task execution. Without understanding these concepts, it is impossible to create high-load systems, data processors, or game engines.
In this article, we will break down the key mechanisms of multithreading in C++, starting with basic std::thread and ending with advanced patterns. You will learn how to avoid data races, properly synchronize threads, and write safe parallel code.
Basics of Multithreading: std::thread and std::jthread
The std::thread class (C++11) is the foundation of multithreaded programming. It 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 worker(int id) { std::cout << "Thread " << id << " started work"; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Thread " << id << " finished";}
int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); return 0;}Key points:
- join() — the main thread waits for the child thread to finish
- detach() — the thread continues running independently (risk of resource leak)
- std::jthread (C++20) — automatically calls join() in the destructor
Always check joinable() before calling join() to avoid double joining.
Thread Synchronization: Mutexes and Locks
The main problem of multithreading is data races, when multiple threads simultaneously modify a shared variable. Mutexes are used for protection.
std::mutex and std::lock_guard
#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; }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Result: " << counter << ""; // 200000 return 0;}std::lock_guard automatically releases the mutex when leaving the scope. For more flexible control, use std::unique_lock.
Deadlock and std::scoped_lock
A deadlock occurs when two threads are waiting for each other. The solution is to use std::scoped_lock (C++17), which acquires multiple mutexes atomically:
std::mutex m1, m2;
void safe_function() { std::scoped_lock lock(m1, m2); // safe work with two resources}Parallel Algorithms and std::async
For executing asynchronous tasks without manual thread management, use std::async. It returns a std::future from which you can obtain the result.
#include <iostream>#include <future>#include <chrono>
int compute(int n) { std::this_thread::sleep_for(std::chrono::seconds(n)); return n * n;}
int main() { auto future1 = std::async(std::launch::async, compute, 2); auto future2 = std::async(std::launch::async, compute, 3); std::cout << "Results: " << future1.get() << ", " << future2.get() << ""; // 4, 9 return 0;}Launch policies:
std::launch::async— launch in a new threadstd