Concurrency in C++: From Threads to Coroutines and async/await

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

Introduction: Why Concurrency in C++ Is Not Scary, But Interesting?

\n\n

Once 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\n

In 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\n

Classic: Multithreading with std::thread

\n\n

Let'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.

\n\n

Simple Example: Parallel Calculation

\n\n

Imagine 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\n

Here 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
\n\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\n

C++20 Revolution: Coroutines (Goroutines)

\n\n

Coroutines 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\n

Many 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\n

How Does It Work?

\n\n

A 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:

\n\n
#include <iostream>\n#include <coroutine>\n#incl                

Blogs

Book Recommendations