Допустим, производитель публикует данные через release-барьер, а потребитель читает их после acquire-барьера: при каком условии такая схема создаёт отношение happens-before?
Такая схема создаёт happens-before, только если между барьерами есть атомарная переменная-переносчик: атомарная операция после release-барьера должна записать значение, которое затем прочитала атомарная операция перед acquire-барьером. Само наличие двух барьеров или совпадение значений без такого прочтения гарантии не создаёт.
Записи, выполненные до release-барьера, тогда становятся видимыми чтениям после acquire-барьера. Атомарные операции-переносчики могут иметь relaxed-порядок: синхронизацию в этой схеме обеспечивают сочетание барьеров и чтения значения из соответствующей записи или её release-последовательности.
Атомарные барьеры появились как более гибкий инструмент управления порядком памяти, чем указание строгого порядка на каждой атомарной операции. Они позволяют отделить упорядочивание обычных обращений к памяти от конкретной атомарной загрузки или записи.
Это было важно для низкоуровневых алгоритмов, где один атомарный объект используется как сигнал публикации, а связанные с ним обычные данные не должны становиться атомарными. Гибкость достигается ценой более сложного рассуждения о корректности.
Поток-производитель может заполнить обычные поля объекта и затем сообщить потребителю, что данные готовы. Если потребитель увидит сигнал, но не получит гарантии порядка памяти, чтение обычных полей может происходить без требуемой видимости и привести к гонке данных.
Ошибка часто возникает, когда разработчик ставит release-барьер перед атомарной записью, acquire-барьер после атомарного чтения, но не проверяет связь между этими операциями. Если чтение не получило значение из нужной записи или её release-последовательности, барьеры не передают публикацию.
Рассмотрим последовательность в производителе: сначала записываются обычные данные, затем выполняется release-барьер, затем атомарная запись в переменную-сигнал. В потребителе атомарная загрузка должна прочитать значение от этой записи или от её release-последовательности, после чего выполняется acquire-барьер и только затем читаются обычные данные.
В таком случае формируется цепочка порядка: обычная запись производителя sequenced-before release-барьер; барьерная схема синхронизирует производителя с потребителем через атомарный переносчик; acquire-барьер sequenced-before чтение данных потребителем. Итогом становится happens-before от публикации данных к их чтению.
Минимальный пример:
Здесь загрузка ready должна увидеть значение, записанное производителем. Тогда release- и acquire-барьеры образуют необходимую связь, и чтение payload не является гонкой с его публикацией.
Более простой и обычно предпочтительный вариант — использовать release-порядок непосредственно при записи сигнала и acquire-порядок при его чтении. Барьеры оправданы, когда требуется тонко управлять порядком нескольких операций или использовать уже выбранные relaxed-операции в низкоуровневом алгоритме.
Важно, что барьер не превращает обычную переменную в атомарную и не исправляет произвольную гонку. Он лишь участвует в установлении порядка при наличии корректного атомарного переносчика и правильной направленности операций.
В очереди без блокировок производитель записывает пакет в заранее выделенный буфер, а затем меняет атомарный индекс готового элемента. Рассматривались три решения: защищать буфер мьютексом, сделать все поля пакета атомарными или использовать публикацию через атомарный индекс.
Мьютекс проще для проверки и обычно предпочтительнее при сложной структуре данных, но добавляет блокировки и конкуренцию. Делать весь пакет атомарным может быть дорого, технически невозможно для некоторых типов и не решает автоматически проблему согласованности нескольких полей.
Был выбран атомарный индекс с release/acquire-публикацией: производитель полностью заполняет слот и публикует индекс, потребитель сначала получает индекс с acquire-порядком, затем читает слот. Вариант с явными барьерами возможен, но оставлен только для участка, где требовался relaxed-доступ и команда могла строго доказать связь между индексом и барьерами.
1. Достаточно ли двух барьеров, если потребитель просто прочитал любое значение атомарного переносчика?
Нет. Acquire-барьер должен быть связан с записью, выполненной после release-барьера: атомарная загрузка перед acquire-барьером должна получить значение от этой записи или её release-последовательности. Если загрузка увидела более старое значение, связь публикации не установлена.
2. Может ли release-барьер сам по себе опубликовать обычные данные?
Нет. Release-барьер только запрещает определённым операциям переместиться за него в модели памяти и участвует в синхронизации при наличии подходящей атомарной операции. Нужна последующая атомарная запись, через которую другой поток сможет обнаружить публикацию.
3. Зачем применять барьеры, если можно указать release и acquire на атомарных операциях?
Во многих прикладных случаях прямые release/acquire-порядки проще для чтения, анализа и сопровождения, поэтому они предпочтительны. Барьеры дают дополнительную гибкость: атомарные операции могут оставаться relaxed, а граница упорядочивания располагается отдельно. Компромисс — повышенный риск ошибки: необходимо доказать не только порядок операций, но и факт чтения нужного значения атомарным переносчиком.