Каким образом операция RMW с порядком relaxed может продолжить release-последовательность атомика?
В стандарте C++20 и новее операция атомарного чтения-изменения-записи (RMW) с порядком relaxed может продолжить release-последовательность, начатую предыдущей release-операцией над тем же атомиком. Если acquire-загрузка прочитала значение, записанное такой RMW, она синхронизируется с исходной release-операцией и получает видимость всех записей, выполненных до неё.
Это работает только для RMW: обычная relaxed-запись не продолжает release-последовательность в современном определении стандарта.
Модель памяти C++11 формализовала способы публикации данных между потоками без обязательной блокировки каждого доступа. Пара release/acquire решает задачу публикации: один поток завершает подготовку обычных данных, а другой после acquire-загрузки безопасно их читает.
Release-последовательность нужна для цепочек атомарных переходов состояния. Промежуточному потоку не требуется повторно публиковать уже подготовленные данные операцией release: достаточно выполнить RMW над тем же атомиком, сохранив связь с исходной публикацией.
Пусть один поток заполняет обычные данные и release-записью переводит атомарное состояние из 0 в 1. Затем другой поток увеличивает это состояние до 2 через relaxed-RMW, а третий поток acquire-загрузкой видит 2.
Неверно считать, что relaxed-операция сама по себе публикует обычные данные. Результат обусловлен тем, что RMW находится в release-последовательности исходной release-записи. Если связь будет нарушена, чтение обычных данных может стать гонкой данных или наблюдать неактуальное состояние.
В порядке модификаций одного атомика операции образуют единую последовательность. Release-операция становится головой release-последовательности, а следующие за ней атомарные RMW над тем же объектом входят в эту последовательность, независимо от потока, который их выполняет.
Acquire-загрузка синхронизируется с release-операцией, если она прочитала значение, записанное самой release-операцией или одной из операций её release-последовательности. Поэтому все действия, предшествовавшие release в исходном потоке, происходят до действий после acquire в потоке-получателе (happens-before).
Минимальный пример:
Здесь store(1, release) публикует data, а fetch_add(1, relaxed) создаёт следующее значение в release-последовательности. Загрузка, прочитавшая 2, получает синхронизацию с исходной release-записью.
Важное ограничение: acquire-загрузка должна прочитать значение от release-операции или от RMW в её последовательности. Само наличие более поздней операции в реальном времени недостаточно. Также обычная relaxed-запись между release и acquire не обладает свойством продолжения последовательности и может разорвать требуемую связь.
Компромисс состоит в том, что RMW часто дешевле полноценной блокировки и позволяет строить атомарные счётчики, очереди состояний и промежуточные этапы протокола. Однако такой код сложнее проверять: нужно доказать непрерывность release-последовательности, правильный выбор атомика и отсутствие доступа к данным до acquire-события.
В конвейере обработки один поток записывает пакет в заранее выделенный буфер и публикует состояние готов. Служебный поток должен увеличить счётчик этапов, не изменяя сам пакет, после чего потребитель начинает обработку.
Рассматривались следующие варианты:
relaxed: минимизирует требования к промежуточному потоку, но корректен только при сохранении release-последовательности.Был выбран третий вариант: исходный поток выполняет release-публикацию, промежуточный — relaxed-RMW, а потребитель использует acquire-загрузку. При этом команда зафиксировала правило: никакие обычные записи в атомарное состояние не допускаются между публикацией и завершающим RMW.
Нет. В C++20 и новее продолжение обеспечивают именно атомарные операции RMW: например, fetch_add, exchange или успешный compare_exchange. Обычная store(relaxed) не выполняет чтение-изменение-запись и не продолжает последовательность исходной release-операции.
Да. Release-последовательность включает саму операцию, которая её возглавляет. Поэтому acquire-загрузка, прочитавшая значение непосредственно от release-записи, синхронизируется с ней так же, как загрузка значения от последующего RMW.
Не обязательно. Relaxed-RMW не является release-операцией и сам по себе не устанавливает связь с предшествующими обычными записями. Видимость таких данных появляется в описанной схеме только потому, что RMW входит в release-последовательность, начатую более ранней release-операцией; без этой головы нужен release-порядок или другая синхронизация.