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

Паттерны параллелизма: пулы потоков и задача "производитель-потребитель"

Начало урока

Паттерны параллелизма: пулы потоков и задача "производитель-потребитель"

От сырых потоков к переиспользуемым конструкциям без взаимных блокировок

Главная идея: Вы уже умеете запускать std::thread, защищать общие данные с помощью std::mutex и рассуждать о модели памяти через атомарные операции. Это примитивы. Реальные системы редко создают по потоку на задачу и редко пишут синхронизацию вручную в каждом месте вызова. Вместо этого они собирают несколько переиспользуемых паттернов: пул потоков, владеющий фиксированным набором рабочих потоков, потокобезопасную очередь, передающую работу между ними, и рукопожатие производитель-потребитель, построенное на std::condition_variable. Освойте эти три вещи — и вы сможете построить почти любой параллельный конвейер, не теряя потоки, не занимаясь активным ожиданием и не создавая гонок за общими данными.
К концу вы сможете:
  • объяснить, почему создание одного потока на задачу не масштабируется и что решает пул потоков;
  • построить ограниченную потокобезопасную очередь на std::mutex и std::condition_variable;
  • реализовать паттерн "производитель-потребитель" с корректной блокировкой и пробуждением;
  • собрать небольшой пул потоков, принимающий задачи и возвращающий std::future;
  • распознавать и предотвращать классические опасности: потерянные и ложные пробуждения, взаимные блокировки и гонки данных.

1. Почему не один поток на задачу?

Наивный подход к параллелизму такой: на каждую единицу работы вызывать std::thread(doWork). Это легко написать и это почти всегда неверно при масштабировании. Потоки не бесплатны:

  • Стоимость создания: порождение потока ОС стоит микросекунд и отдельного стека (часто резервируется 1 МБ). Обработчик, создающий поток на каждый запрос, платит это снова и снова.
  • Избыточная подписка (oversubscription): если 10 000 задач порождают по потоку на 8-ядерной машине, планировщик буксует, переключая контекст между гораздо большим числом потоков, чем ядер, и пропускная способность падает.
  • Неограниченное потребление ресурсов: ничто не ограничивает количество потоков, поэтому всплеск работы может исчерпать память или дескрипторы ОС.

Пул потоков переворачивает эту связь. Он один раз создаёт фиксированное число долгоживущих рабочих потоков (обычно std::thread::hardware_concurrency()), а затем подаёт им поток небольших задач через общую очередь. Рабочие потоки крутятся в цикле: берут задачу, выполняют её, повторяют. Потоки переиспользуются, их число ограничено, а очередь естественным образом сглаживает всплески. Это паттерн почти любого сервера, асинхронной среды выполнения на потоках и библиотеки параллельных алгоритмов.

Сдвиг в мышлении: перестаньте думать, что "поток это задача", и начните думать, что "поток — это работник, потребляющий задачи". Задачи становятся данными; потоки становятся фиксированной инфраструктурой.

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

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

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

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