Multihilo y asincronía en C++: guía detallada
What is it and why? (a real-life analogy)
Imagine you are a head chef in a huge kitchen. You have one assistant. You need to prepare three dishes: soup, salad, and dessert.
Sequential work (without multithreading): You tell the assistant: "First, chop the vegetables for the soup. Finished? Now chop the salad. Done? Now whip the cream for the dessert." Everything is done strictly one after another. If the assistant is chopping vegetables, you stand and wait — you cannot wash dishes or check orders at the same time. This is slow and inefficient.
Multithreading: You hire three assistants (threads). The first chops for the soup, the second for the salad, the third whips the cream. Everyone works simultaneously! You (the main thread) only distribute tasks and make sure they don't interfere with each other (for example, don't grab the same knife). The food is prepared three times faster.
Asynchrony: You give the assistant a task: "Put the soup on to boil for 20 minutes. While it's boiling, don't just stand there — go chop the salad." The assistant starts a long task (boiling) and, without waiting for it to finish, switches to another job. When the soup is ready, the assistant (or a timer) will notify you: "The soup is ready!". You don't waste time waiting.
Main idea: Multithreading is about simultaneous execution (parallelism). Asynchrony is about non-blocking waiting (when one task waits, another works).
Why is this needed in programming?
- Speed: Divide a complex task into parts and solve them simultaneously (e.g., image processing, video rendering).
- Interface responsiveness: While the program downloads a file from the internet (a long operation), the interface should not "freeze". The download thread works in the background, while the main thread draws buttons and responds to clicks.
- Efficiency: Use all processor cores at 100%, rather than keeping 7 out of 8 cores idle.
Theory: how it works internally
Processes and threads
Process — is a running program with its own memory. Two processes (e.g., a browser and a game) are isolated from each other: if one crashes, the second continues to work.
Thread — is a "lightweight" process inside a process. All threads of one process have shared memory. They can read and modify the same variables. This is convenient for data exchange, but dangerous: if two threads modify one variable simultaneously, a data race occurs.
How the processor executes threads
If the processor has 4 cores, it can execute 4 threads truly simultaneously (in parallel). If there are more threads than cores (e.g., 10 threads on 4 cores), the processor creates an illusion of simultaneity: it quickly switches between threads (context switching), giving each a tiny time quantum. To a human, this looks like simultaneous work.
The synchronization problem
Imagine: two threads want to deposit money into a bank account. Both read the balance (1000 rubles), both add 500, both write 1500. But it should be 2000! This is a data race. For protection, mutexes are used — "locks". A thread acquires the mutex, works with the data, releases it. Another thread waits until the lock is released.
Asynchrony at the OS level
Asynchronous operations (reading a file, a server request) are usually performed through the operating system. You tell the OS: "Read this file and, when you finish, call my function (callback)". While the OS reads the file (using DMA — Direct Memory Access, without CPU involvement), your thread can do other work. This is more efficient than creating a separate thread for waiting.
Syntax in C++: all forms and variants
In C++, multithreading appeared in the C++11 standard. Before that, OS libraries were used (pthreads on Linux, WinAPI on Windows). Modern C++ provides cross-platform tools.
Main components (header file <thread>)
| Class/Function | Description |
|---|---|
std::thread |
Creates a new thread. Takes a function (or lambda) to execute. |
std::mutex |
Mutex (mutual exclusion). Protects shared data. |
std::lock_guard |
Smart wrapper for a mutex. Automatically locks on creation and unlocks on destruction. |
std::unique_lock |
A more flexible version of lock_guard. Can be locked and unlocked manually. |
std::atomic |
Atomic variables. Operations on them (read, write) are indivisible — no mutex needed. |
std::async |
Launches an asynchronous task. Returns a std::future. |
std::future |
Stores the result of an asynchronous operation, which will be obtained in the future. |
std::promise |
Allows manually setting a value for a future (from another thread). |
std::condition_variable |
Condition variable. Allows a thread to wait until another thread fulfills a condition. |
Creating a thread
#include <iostream>
#include <thread>
// Function to be executed by the thread
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// Create a thread t that executes the function hello
std::thread t(hello);
// Main thre