Многопоточность
Безопасное параллельное выполнение кода: потоки, мьютексы, условные переменные и предотвращение взаимоблокировок
pthreads или потоков Win32.
- запускать работу в
std::threadи управлять её временем жизни черезjoin/detach- и предпочитатьstd::jthreadиз C++20; - объяснить, что такое гонка данных и почему это неопределённое поведение;
- защищать общее состояние с помощью
std::mutexи RAII-обёртокlock_guardиunique_lock; - координировать потоки через
std::condition_variableс идиомой ожидания по предикату; - распознавать взаимоблокировку и избегать её через упорядочивание захвата и
std::scoped_lock.
1. Конкурентность и параллелизм, и std::thread
Конкурентность - это свойство структуры: несколько задач выполняются в перекрывающиеся промежутки времени. Параллелизм - это свойство исполнения: задачи буквально работают в один момент на разных ядрах. Потоки позволяют выразить конкурентность; станет ли она настоящим параллелизмом, зависит от оборудования. Используйте std::thread::hardware_concurrency() как подсказку о числе доступных ядер.
std::thread (из <thread>) начинает выполнять свой вызываемый объект сразу при конструировании. Вы передаёте любой вызываемый объект - указатель на функцию, лямбду, функтор - плюс его аргументы. Аргументы по умолчанию копируются в поток; чтобы передать по ссылке, оберните их в std::ref.
#include <thread>
#include <iostream>
void greet(int id) { std::cout << "hello from " << id << "\n"; }
int main() {
std::thread t(greet, 42); // запускается сейчас
t.join(); // ждём завершения
}
Прежде чем std::thread будет уничтожен, вы обязаны решить его судьбу: join() блокирует до завершения потока, а detach() отпускает его работать независимо и разрывает ваш дескриптор. Если уничтожить присоединяемый (joinable) поток, не сделав ни того ни другого, деструктор вызовет std::terminate() и программа аварийно завершится. Это суровое правило существует, чтобы вы случайно не потеряли работающий поток.
std::jthread. jthread ("присоединяющийся поток") устраняет ловушку: его деструктор автоматически вызывает join, так что забытый join больше не завершает программу. Он также несёт std::stop_token, чтобы поток можно было кооперативно попросить остановиться. В новом коде на C++20 предпочитайте std::jthread.