Программирование C++Современный C++Разработчик многопоточных C++-систем

В многофазном параллельном алгоритме потоки должны повторно встречаться после каждой фазы, причём некоторые...

В многофазном параллельном алгоритме потоки должны повторно встречаться после каждой фазы, причём некоторые из них могут окончательно выйти. Какой механизм C++20 выбрать для такой синхронизации?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте std::barrier. Он синхронизирует повторяющиеся фазы: каждый участник сообщает о завершении текущей фазы, а переход к следующей происходит после всех ожидаемых сообщений.

Если поток больше не будет участвовать в последующих фазах, он должен вызвать arrive_and_drop(). Это уменьшает число участников текущей и будущих фаз и предотвращает вечное ожидание оставшихся потоков.

Исторический контекст

До C++20 для циклической синхронизации обычно комбинировали std::mutex, std::condition_variable и счётчик прибывших потоков. Такой код требовал вручную обрабатывать сброс счётчика, переход между фазами и выход участников, поэтому легко было получить гонку или зависание.

std::latch, появившийся в C++20, решает только одноразовую задачу: после достижения нуля он не перезапускается. Для повторяющихся фаз предназначен именно std::barrier.

Постановка проблемы

Предположим, несколько потоков по очереди выполняют этапы вычисления. Следующий этап нельзя начинать, пока все активные участники не завершили предыдущий.

Обычный счётчик недостаточен: нужно атомарно завершать фазу, безопасно будить ожидающих потоков и корректно учитывать тех, кто прекращает работу. Ошибка в количестве участников приводит к зависанию на барьере, а преждевременное уничтожение барьера при работающих потоках создаёт неопределённое поведение.

Подробное решение

При создании std::barrier задаётся число ожидаемых участников. В каждой фазе поток может вызвать:

  • arrive_and_wait() — отметить прибытие и ждать завершения фазы;
  • arrive() — отметить прибытие и продолжить работу, получив объект ожидания для последующего ожидания;
  • wait(token) — ждать завершения фазы по токену, полученному из arrive();
  • arrive_and_drop() — завершить текущую фазу и отказаться от участия в будущих.

Когда прибывают все ожидаемые участники, барьер завершает фазу и автоматически переходит к следующей. Поэтому один и тот же объект можно использовать в цикле без ручного сброса счётчика.

У барьера может быть функция завершения фазы. Она вызывается один раз при переходе между фазами и подходит для публикации или подготовки общего результата. Однако сам барьер не защищает произвольные данные от гонок: доступ к данным должен быть организован так, чтобы работа с ними происходила до прибытия, после ожидания или под дополнительной синхронизацией.

Количество участников должно соответствовать реальной логике алгоритма. Если поток просто перестал вызывать arrive_and_wait() вместо arrive_and_drop(), оставшиеся потоки будут ждать его бесконечно.

Минимальный пример повторяющихся фаз:

#include <barrier> #include <thread> #include <vector> int main() { constexpr int workers_count = 4; constexpr int phases = 3; std::barrier sync(workers_count); std::vector<std::jthread> workers; for (int i = 0; i < workers_count; ++i) { workers.emplace_back([&sync] { for (int phase = 0; phase < phases; ++phase) { // Работа текущей фазы. sync.arrive_and_wait(); } }); } }

В отличие от std::condition_variable, барьер инкапсулирует счётчик участников и переходы между фазами. В отличие от std::latch, он предназначен для многократного использования. В отличие от мьютекса, он не обеспечивает взаимное исключение и не заменяет защиту совместно изменяемого состояния.

Ситуация из практики

В численном алгоритме четыре рабочих потока обрабатывают части матрицы. После каждой итерации все части должны быть готовы, прежде чем потоки начнут следующую итерацию. Иногда один поток завершает обработку раньше остальных и больше не должен участвовать.

Вариант с condition_variable даёт гибкость, но требует вручную поддерживать счётчик, номер фазы и предикат ожидания. Вариант с std::latch не подходит, потому что latch одноразовый. Простой мьютекс также не решает задачу: он сериализует доступ, но не выражает условие «все участники завершили фазу».

Выбранное решение — std::barrier: активные потоки используют arrive_and_wait(), а уходящий поток — arrive_and_drop(). В результате следующие фазы не ждут завершившийся поток, а код не содержит ручного сброса счётчика; риск остаётся только в необходимости правильно определить момент выхода и жизненный цикл самого барьера.

Что кандидаты часто упускают

1. Чем std::barrier отличается от std::latch не только названием?

std::latch имеет одно уменьшающееся значение и после достижения нуля не может быть использован повторно. Он подходит для ожидания завершения набора независимых одноразовых задач.

std::barrier состоит из последовательности фаз. После прибытия всех текущих участников он автоматически начинает новую фазу, а arrive_and_drop() позволяет уменьшать состав участников. Поэтому выбор определяется не только количеством потоков, но и повторяемостью синхронизации.

2. Что произойдёт, если один поток выйдет из цикла без arrive_and_drop()?

В следующей фазе барьер продолжит ожидать исходное число участников. Потоки, которые остались в цикле, не смогут завершить фазу, поскольку отсутствующий поток больше не уменьшит счётчик.

Это не исключение и не автоматическое обнаружение ошибки: типичный результат — блокировка на arrive_and_wait(). Если число участников меняется динамически, выход должен быть частью протокола и явно оформляться через arrive_and_drop().

3. Можно ли использовать std::barrier для защиты общего счётчика вместо мьютекса?

Нет. Барьер координирует момент перехода между фазами, но не делает операции над общим объектом атомарными и не обеспечивает взаимное исключение.

Он может быть частью корректной схемы: например, потоки изменяют разные элементы массива в первой фазе, встречаются на барьере, а затем читают уже подготовленные данные. Если несколько потоков изменяют одну переменную одновременно, всё равно нужны атомарные операции или отдельный мьютекс.