Перейти к содержанию

Начало урока

Многопоточность

Безопасное параллельное выполнение кода: потоки, мьютексы, условные переменные и предотвращение взаимоблокировок

Главная идея: Поток - это независимый ход выполнения внутри одного процесса. Все потоки разделяют одно адресное пространство - те же глобальные переменные, ту же кучу - и именно это делает потоки одновременно мощными и опасными. Совместный доступ позволяет потокам дёшево сотрудничать; но он же означает, что два потока, обращающиеся к одним данным одновременно, могут их испортить. Современный C++ (начиная с C++11, доработанный в C++17 и C++20) даёт переносимую библиотеку потоков, так что больше не нужны платформенные API вроде 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() и программа аварийно завершится. Это суровое правило существует, чтобы вы случайно не потеряли работающий поток.

C++20: std::jthread. jthread ("присоединяющийся поток") устраняет ловушку: его деструктор автоматически вызывает join, так что забытый join больше не завершает программу. Он также несёт std::stop_token, чтобы поток можно было кооперативно попросить остановиться. В новом коде на C++20 предпочитайте std::jthread.

Продолжите этот урок на YourAIZone

В полном уроке — объяснение целиком, разобранные примеры, частые ошибки, тесты с мгновенной проверкой и ИИ-репетитор. Начните сразу как гость или создайте аккаунт, чтобы сохранять прогресс.

В полном уроке

  • Объяснение целиком
  • Разобранные примеры
  • Частые ошибки и как их избежать
  • Тестовые вопросы: 15
  • ИИ-репетитор, который отвечает на вопросы по теме
  • Отслеживание прогресса для ученика, родителей и учителя