Сравнение: чем принципиально отличается std::barrier от std::latch при организации повторяющихся фаз работы потоков?
std::latch предназначен для одноразового ожидания достижения счётчиком нуля: его нельзя сбросить для следующего цикла. std::barrier синхронизирует повторяющиеся фазы: после прибытия всех участников текущая фаза завершается, а объект автоматически переходит к следующей.
Поэтому для однократного ожидания завершения набора операций выбирают latch, а для циклического обмена «все закончили фазу — начинаем следующую» — barrier.
До появления этих примитивов в C++20 одноразовую координацию часто реализовывали через условные переменные, мьютексы или атомарный счётчик с ручной проверкой предиката. Для повторяющихся фаз приходилось самостоятельно учитывать номер итерации, число участников, повторные уведомления и уход потоков.
std::latch и std::barrier стандартизировали два разных сценария координации. Это уменьшает объём ручной синхронизации и делает намерение программы явным.
Если применить latch внутри цикла, после достижения нулевого счётчика он останется завершённым: последующие вызовы ожидания будут проходить сразу. Такой объект не обеспечивает разделение итераций и может привести к тому, что поток начнёт следующую фазу, пока другие ещё работают.
Если использовать barrier для одноразового события, это возможно, но семантика будет шире необходимой. Кроме того, нужно корректно учитывать количество участников: фаза не завершится, пока не прибудут все зарегистрированные участники либо кто-то не вызовет операцию выхода из барьера.
У std::latch есть начальное значение счётчика. Операция count_down уменьшает его, wait блокирует поток до достижения нуля, а count_down_and_wait совмещает уменьшение счётчика с ожиданием. Счётчик нельзя увеличить или вернуть к исходному значению, поэтому latch является одноразовым.
std::barrier задаёт число ожидаемых участников каждой фазы. Участник может вызвать arrive_and_wait, уменьшив число ещё не прибывших участников и ожидая остальных. После завершения фазы ожидающие потоки разблокируются, а барьер становится готов к следующей фазе.
Участник может вызвать arrive_and_drop, если он больше не будет участвовать в будущих фазах. Это уменьшает число участников для текущего завершения и последующих фаз. Простого «пропуска» фазы без уведомления недостаточно: остальные потоки продолжат ждать недостающего участника.
Завершение фазы barrier также предоставляет точку синхронизации между потоками. Операции, выполненные до завершения фазы, становятся упорядоченными относительно потоков, разблокированных этим завершением. Однако барьер не заменяет защиту данных при одновременном доступе внутри самой фазы.
В примере initialized используется один раз для ожидания готовности обоих потоков. phase_end повторно разделяет две фазы: каждый поток не перейдёт к следующей итерации, пока оба не достигнут барьера.
В симуляции физической системы несколько потоков обрабатывают свои участки сетки. После каждой итерации они должны завершить вычисления, обменяться граничными значениями и только затем перейти к следующей итерации.
Условная переменная дала бы гибкость, но потребовала бы защищать счётчик, хранить номер фазы и проверять предикат в цикле. Атомарный счётчик был бы дешевле по структуре, но всё равно потребовал бы самостоятельно реализовать повторное открытие фаз и корректный уход потока.
std::latch здесь не подходит: он одноразовый. Выбранный std::barrier прямо выражает циклическую синхронизацию, автоматически начинает следующую фазу и позволяет явно обработать поток, который прекращает участие, через arrive_and_drop. Это снижает риск ошибок в протоколе и делает код проще для проверки.
1. Можно ли повторно использовать std::latch, если его счётчик уже достиг нуля?
Нет. std::latch не поддерживает сброс или увеличение счётчика после создания. Для нового одноразового ожидания нужен новый объект latch либо другой примитив, например std::barrier для повторяющихся фаз.
2. Что должен сделать поток, который покидает циклический алгоритм до следующей фазы?
Он должен сообщить об уходе через arrive_and_drop. Если поток просто завершится или перестанет вызывать операции барьера, оставшиеся участники могут навсегда заблокироваться, ожидая его прибытия. После arrive_and_drop число участников последующих фаз уменьшается.
3. Выполняется ли функция завершения фазы std::barrier отдельным служебным потоком?
Такой гарантии нет. Функция завершения вызывается ровно один раз для фазы, но её выполнение не следует считать работой выделенного служебного потока или полагаться на конкретный идентификатор потока. Она должна быть короткой и не блокироваться надолго, иначе переход всех участников к следующей фазе будет задержан.