В каких пределах std::latch синхронизирует данные между участниками после завершения ожидания?
После возврата из std::latch::wait() поток может безопасно читать обычные данные, записи в которые были выполнены другими потоками до их операций уменьшения счётчика. Завершение фазы latch синхронизируется с такими ожидающими потоками и устанавливает отношение happens-before.
Эта гарантия не распространяется на записи, выполненные после count_down(), и сам std::latch нельзя повторно использовать: его счётчик только уменьшается и после достижения нуля не восстанавливается.
До появления C++20 одноразовую координацию группы потоков часто реализовывали вручную через мьютекс, условную переменную и счётчик завершившихся задач. Такой код требовал отдельно защищать счётчик, проверять предикат в цикле и корректно организовывать публикацию результатов.
std::latch появился как специализированный примитив для одноразового события: несколько участников завершают работу, а один или несколько потоков ждут, пока заданное число сигналов не будет получено.
Пусть рабочие потоки записывают результаты в обычные поля или элементы массива, а затем сообщают о завершении через latch. Если потребитель просто проверяет числовой счётчик без синхронизации, возникает гонка доступа к счётчику или к результатам.
Неверно также считать, что достижение нулевого счётчика делает видимыми любые действия потока. Видимыми гарантированно становятся действия, предшествующие соответствующему сигналу завершения; последующие записи требуют отдельного протокола.
Когда последний необходимый участник уменьшает счётчик до нуля, latch завершает свою единственную фазу. Завершение этой фазы strongly happens-before возврата из wait(). Поэтому записи, выполненные до count_down() в участвующих потоках, становятся доступными потоку после успешного ожидания без дополнительных атомиков или мьютекса.
count_down() не блокирует вызывающий поток: он только уменьшает счётчик и, возможно, завершает latch. wait() блокируется до нулевого значения, а try_wait() лишь проверяет состояние без блокировки. arrive_and_wait() уменьшает счётчик и затем ждёт завершения.
Гарантия распространяется только на операции, которые действительно предшествуют сигналу завершения в том же потоке. Если поток вызвал count_down(), а затем продолжил изменять тот же объект, читатель после wait() не получает права одновременно читать эти последующие изменения: это может привести к гонке данных.
Latch одноразовый. Для повторяющихся раундов нужно использовать std::barrier, отдельные latch или другой протокол синхронизации. Кроме того, число сигналов не должно логически превышать начальное значение счётчика: корректный алгоритм должен вызывать уменьшение ровно необходимое число раз.
Минимальный пример публикации результатов:
После wait() чтение result[0] и result[1] безопасно с точки зрения модели памяти: обе записи произошли до count_down(), завершившего latch. Само уничтожение jthread здесь лишь корректно дожидается завершения потоков; гарантию публикации результатов даёт именно latch.
В сервисе два потока параллельно строят части индекса, записывая результаты в разные элементы общего массива. Координатор должен начать сборку только после завершения обоих вычислений.
Можно использовать общий атомарный счётчик с циклическим ожиданием. Это не требует блокировки, но расходует процессорное время и не выражает явно одноразовую природу события. Связка мьютекса и условной переменной гибче, но требует больше кода и аккуратной проверки предиката.
Выбран std::latch с начальным счётчиком два: каждый рабочий поток записывает свою часть массива и вызывает count_down(), координатор вызывает wait(). Решение кратко выражает протокол, блокирует координатора вместо активного ожидания и обеспечивает публикацию записанных результатов. Если индекс строится повторно в цикле, вместо latch потребуется std::barrier или новый объект latch для каждой итерации.
count_down() после начала записи, чтобы читатель увидел всю запись?Нет. Для гарантии запись должна быть упорядочена до count_down() в том же потоке. Если запись выполняется после сигнала, возврат другого потока из wait() не синхронизирует эту запись с чтением. При пересечении чтения и последующей записи обычного объекта возможна гонка данных и неопределённое поведение.
std::latch от одновременной записи нескольких потоков в один объект?Нет. Latch координирует достижение точки завершения, но не делает саму работу потокобезопасной. Если два участника одновременно изменяют один обычный объект, им всё равно нужен мьютекс, атомик или заранее обеспеченное разделение данных. После wait() безопасно читать результаты можно только при отсутствии других конкурентных записей.
std::latch для следующего раунда задач?Нет. После достижения нуля latch остаётся завершённым, а операции уменьшения не возвращают его в исходное состояние. Для повторяющихся фаз нужен новый latch на каждую фазу либо std::barrier, который поддерживает повторное прохождение фаз и может выполнять функцию завершения фазы.