Программирование C++МногопоточностьРазработчик C++ системного программного обеспечения

При последовательных relaxed загрузках одной атомарной переменной может ли второй результат относиться к бо...

При последовательных relaxed-загрузках одной атомарной переменной может ли второй результат относиться к более старой записи, чем первый?

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

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

Нет. Для одной атомарной переменной последующие загрузки в одном потоке не могут наблюдать записи в порядке, обратном порядку, уже наблюдённому этим потоком. Однако memory_order_relaxed не обеспечивает согласованность между разными атомиками и не делает обычные данные безопасными для конкурентного доступа.

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

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

Модель памяти C++ появилась, чтобы формально описать многопоточность и одновременно допустить эффективные реализации на процессорах с различными правилами переупорядочивания. Атомики решают проблему конкурентного доступа к одной переменной без гонки данных.

Порядок relaxed предназначен для случаев, когда нужна атомарность и согласованность конкретного атомика, но не требуется публикация связанных обычных данных. Это позволяет компилятору и процессору избегать более дорогих ограничений упорядочивания.

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

Пусть один поток изменяет атомарный счётчик, а другой несколько раз читает его с memory_order_relaxed. Ошибочное рассуждение утверждает, что relaxed разрешает второй загрузке увидеть произвольное старое состояние, даже если первая загрузка уже увидела более новую запись.

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

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

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

Две загрузки, выполненные последовательно в одном потоке, связаны отношением sequenced-before, которое является частным случаем happens-before. Правило read-read coherence требует, чтобы вторая загрузка наблюдала ту же запись либо запись, расположенную позже первой в порядке модификаций. Поэтому возврат к более старой записи запрещён.

Это ограничение не означает монотонность числового значения само по себе. Если разные потоки записывают значения 100, затем 3, то наблюдатель может увидеть 100, затем 3: вторая запись новее, хотя число меньше. Гарантируется порядок наблюдённых записей, а не математическое сравнение их значений.

relaxed также не создаёт отношения synchronizes-with. Если поток записал обычное поле, а затем relaxed-записал флаг, relaxed-загрузка флага не делает чтение обычного поля безопасным. Для публикации данных обычно применяют release-запись и acquire-загрузку либо другой механизм синхронизации.

Минимальный пример использует возрастающие значения, поэтому запрет возврата к более старой записи выражается числовым сравнением:

#include <atomic> #include <cassert> #include <thread> std::atomic<int> counter{0}; int main() { std::thread writer([] { counter.store(1, std::memory_order_relaxed); counter.store(2, std::memory_order_relaxed); counter.store(3, std::memory_order_relaxed); }); int first = counter.load(std::memory_order_relaxed); int second = counter.load(std::memory_order_relaxed); assert(second >= first); writer.join(); }

Утверждение относится к двум загрузкам одного потока и возрастающим записям. Оно не утверждает, что читатель обязательно увидит 3, а также не распространяется на согласованность нескольких атомиков.

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

В сервисе поток обработки публикует возрастающий номер обработанного элемента, а поток мониторинга периодически читает этот номер. Для самого счётчика достаточно relaxed: монитор не должен наблюдать возврат к более старой записи, а публикация обычного состояния обработки через этот счётчик не требуется.

Вариант с мьютексом проще расширить на несколько связанных полей, но добавляет блокировки и риск задержек. seq_cst даёт более сильное глобальное упорядочивание атомарных операций, однако для одного независимого счётчика это избыточно. Выбран relaxed, потому что нужна только атомарность и согласованное наблюдение одной переменной; если монитор должен безопасно читать опубликованный объект, применяется release/acquire или мьютекс.

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

  1. Означает ли read-read coherence, что relaxed-загрузка всегда возвращает последнее записанное значение?

Нет. Она может увидеть не самую свежую запись с точки зрения реального времени. Гарантия касается невозможности наблюдать более старую запись после более новой в последовательных загрузках одного потока, а не мгновенной видимости последнего значения.

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

  1. Можно ли перенести эту гарантию на две разные атомарные переменные?

Нет. У каждой атомарной переменной свой порядок модификаций, а relaxed не создаёт единого глобального порядка операций над разными объектами.

Например, наблюдатель может увидеть обновление одного счётчика без обновления другого. Если алгоритму нужна согласованная публикация набора значений, используют release/acquire с корректным протоколом, мьютекс либо более сильное упорядочивание, если оно действительно требуется.

  1. Достаточно ли relaxed, если после счётчика нужно читать обычные данные?

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

Для публикации используют release-операцию при публикации и acquire-операцию при наблюдении соответствующего значения. Тогда записи перед release становятся видимыми после успешного acquire, при условии что acquire наблюдает нужную release-последовательность; альтернативой является мьютекс.