Введение в конкурентность и многопоточность в C++
Современные приложения требуют максимальной производительности. Конкурентность и многопоточность в C++ позволяют эффективно использовать многоядерные процессоры, ускоряя выполнение задач. Без понимания этих концепций невозможно создавать высоконагруженные системы, обработчики данных или игровые движки.
В этой статье мы разберем ключевые механизмы многопоточности в C++, начиная с базовых std::thread и заканчивая продвинутыми паттернами. Вы узнаете, как избежать гонок данных, правильно синхронизировать потоки и писать безопасный параллельный код.
Основы многопоточности: std::thread и std::jthread
Класс std::thread (C++11) — фундамент многопоточного программирования. Он позволяет запустить функцию или callable-объект в отдельном потоке. Рассмотрим простейший пример:
#include <iostream>
#include <thread>
void worker(int id) {
std::cout << "Поток " << id << " начал работу\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Поток " << id << " завершен\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}
Ключевые моменты:
- join() — основной поток ждет завершения дочернего потока
- detach() — поток продолжает работу независимо (риск утечки ресурсов)
- std::jthread (C++20) — автоматически вызывает join() в деструкторе
Всегда проверяйте joinable() перед вызовом join(), чтобы избежать двойного присоединения.
Синхронизация потоков: мьютексы и блокировки
Главная проблема многопоточности — гонки данных, когда несколько потоков одновременно изменяют общую переменную. Для защиты используются мьютексы.
std::mutex и 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 << "Результат: " << counter << "\n"; // 200000
return 0;
}
std::lock_guard автоматически освобождает мьютекс при выходе из области видимости. Для более гибкого управления используйте std::unique_lock.
Deadlock и std::scoped_lock
Взаимная блокировка (deadlock) возникает, когда два потока ждут друг друга. Решение — использовать std::scoped_lock (C++17), который захватывает несколько мьютексов атомарно:
std::mutex m1, m2;
void safe_function() {
std::scoped_lock lock(m1, m2);
// безопасная работа с двумя ресурсами
}
Параллельные алгоритмы и std::async
Для выполнения асинхронных задач без ручного управления потоками используйте std::async. Он возвращает std::future, из которого можно получить результат.
#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 << "Результаты: " << future1.get()
<< ", " << future2.get() << "\n"; // 4, 9
return 0;
}
Политики запуска:
std::launch::async— запуск в новом потокеstd::launch::deferred— ленивое выполнение при вызове get()
Параллельные алгоритмы STL (C++17)
Многие алгоритмы STL получили параллельные версии. Укажите политику выполнения std::execution::par для автоматической параллелизации:
#include <algorithm>
#include <execution>
#include <vector>
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 0);
// Параллельная сортировка
std::sort(std::execution::par, data.begin(), data.end());
Атомарные операции и барьеры памяти
Для простых операций (инкремент, флаги) мьютексы избыточны. Используйте std::atomic — это обеспечивает атомарность без блокировок.
#include <atomic>
std::atomic<int> atomic_counter{0};
void atomic_increment() {
for (int i = 0; i < 100000; ++i) {
atomic_counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(atomic_increment);
std::thread t2(atomic_increment);
t1.join();
t2.join();
std::cout << atomic_counter.load() << "\n"; // 200000
return 0;
}
Порядки памяти (memory order):
memory_order_relaxed— минимальные гарантии, максимальная скоростьmemory_order_acquire/release— для синхронизации между потокамиmemory_order_seq_cst— самый строгий (по умолчанию)
Барьеры памяти и std::atomic_thread_fence
Иногда нужно синхронизировать не атомарные переменные. Для этого существуют барьеры памяти:
std::atomic<bool> flag{false};
int shared_data = 0;
void writer() {