Introduction: Why Concurrency in C++ Is Not Scary, But Interesting?
\n\nOnce upon a time, in the era of single-core processors, programs executed strictly sequentially. Today, even your smartphone has 8 cores, and servers have dozens. Making all these cores work for you — that's where the real magic of performance lies. And C++, one of the most powerful and low-level languages, gives us a whole arsenal of tools for this. But how not to get confused? Threads, coroutines (often called "goroutines" by analogy with Go), async/await — what to choose?
\n\nIn this article, we will break down modern concurrency in C++. You will learn when to use classic threads and when to use lightweight C++20 coroutines, and how async/await simplifies asynchronous programming. Let's go!
\n\nClassic: Multithreading with std::thread
\n\nLet's start with the most obvious — threads. std::thread is direct access to the operating system's system threads. Each thread is a separate execution context that can run on a separate CPU core.
Simple Example: Parallel Calculation
\n\nImagine you need to calculate the sum of squares of numbers from 1 to 10 million. Sequentially — that's slow. Let's parallelize it:
\n\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n result = 0;\n for (int i = start; i <= end; ++i) {\n result += i * i;\n }\n}\n\nint main() {\n const int N = 10'000'000;\n const int NUM_THREADS = 4;\n std::vector<std::thread> threads;\n std::vector<long long> results(NUM_THREADS, 0);\n \n int chunk = N / NUM_THREADS;\n for (int i = 0; i < NUM_THREADS; ++i) {\n int start = i * chunk + 1;\n int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n }\n \n for (auto& t : threads) {\n t.join();\n }\n \n long long total = std::accumulate(results.begin(), results.end(), 0LL);\n std::cout << "Sum of squares: " << total << std::endl;\n return 0;\n}\n\n\nHere we create 4 threads, each calculating its own part of the range. The results are collected into a vector. Everything is simple and predictable. But there are nuances:
\n\n- \n
- Overhead: creating each thread is expensive (stack allocation, system calls). For thousands of small tasks, this is inefficient. \n
- Synchronization: if threads access shared data, mutexes are needed, which can cause blocking and reduce performance. \n
- Limited number: the number of threads should usually not exceed the number of CPU cores (considering hyperthreading). \n
For heavy, long computations, threads are an excellent choice. But for I/O operations (reading files, network requests), they are often overkill. This is where coroutines come into play.
\n\nC++20 Revolution: Coroutines (Goroutines)
\n\nCoroutines in C++20 are not just another feature. They are a fundamental change in how we write asynchronous code. Unlike threads, coroutines are lightweight subroutines that can suspend their execution and resume it later without blocking the OS thread.
\n\nMany developers call them "goroutines" due to their similarity to goroutines in Go, although technically they are different concepts. The main advantage: you can run hundreds of thousands of coroutines on a single thread without losing performance.
\n\nHow Does It Work?
\n\nA coroutine is a function that contains the keyword co_await, co_return, or co_yield. The compiler automatically transforms it into a state machine. Here is a basic example:
#include <iostream>\n#include <coroutine>\n#incl