В многофазном параллельном алгоритме потоки должны повторно встречаться после каждой фазы, причём некоторые из них могут окончательно выйти. Какой механизм C++20 выбрать для такой синхронизации?
Используйте 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(), оставшиеся потоки будут ждать его бесконечно.
Минимальный пример повторяющихся фаз:
В отличие от 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 для защиты общего счётчика вместо мьютекса?
Нет. Барьер координирует момент перехода между фазами, но не делает операции над общим объектом атомарными и не обеспечивает взаимное исключение.
Он может быть частью корректной схемы: например, потоки изменяют разные элементы массива в первой фазе, встречаются на барьере, а затем читают уже подготовленные данные. Если несколько потоков изменяют одну переменную одновременно, всё равно нужны атомарные операции или отдельный мьютекс.