В чём принципиальное различие между std::atomic_signal_fence и std::atomic_thread_fence при обмене данными между потоками?
std::atomic_signal_fence ограничивает перестановку операций только на уровне компилятора и предназначен прежде всего для взаимодействия с обработчиками сигналов. Он не устанавливает аппаратный порядок доступа и не создаёт синхронизацию между потоками. std::atomic_thread_fence может участвовать в межпоточном упорядочивании, однако сама по себе также не публикует данные: ей нужны связанные атомарные операции.
Модель памяти C++ появилась для описания поведения оптимизирующего компилятора и процессоров с переупорядочиванием операций. Одной из задач было отделить ограничения для компилятора от ограничений, которые должны быть обеспечены аппаратурой.
Поэтому стандарт различает барьеры для потока выполнения и барьеры, относящиеся к взаимодействию с обработчиком сигнала. Такое разделение предотвращает ошибочное применение лёгкого компиляторного барьера там, где требуется межпоточная синхронизация.
Предположим, один поток записывает обычные данные, а затем устанавливает атомарный флаг. Другой поток ждёт флаг и после этого читает данные. Если заменить настоящую публикацию через release/acquire на atomic_signal_fence, компилятор может не переставить операции, но это не создаёт отношения happens-before между потоками.
В результате чтение обычных данных во втором потоке может образовать гонку данных, что означает неопределённое поведение. Атомарность самого флага не делает соседние обычные данные автоматически безопасными.
atomic_signal_fence является компиляторным барьером. Он не обязан генерировать инструкцию барьера памяти и не устанавливает порядок, наблюдаемый другим процессором. Его назначение — не дать компилятору свободно перемещать определённые операции через границу барьера в сценариях, связанных с обработчиком сигнала.
atomic_thread_fence задаёт ограничения для межпоточного порядка. На слабых архитектурах он может приводить к аппаратным инструкциям барьера, а на более строгих архитектурах — оказаться дешевле или быть оптимизирован компилятором. Но отдельный fence не передаёт данные другому потоку автоматически.
Для публикации обычно используют атомарную переменную как точку синхронизации: производитель делает release-запись флага, а потребитель выполняет acquire-чтение, увидевшее эту запись или соответствующую release-последовательность. Тогда записи обычных данных, предшествующие публикации, становятся видимыми после acquire.
Этот пример некорректен: два atomic_signal_fence не синхронизируют потоки, поэтому доступ к data не защищён. Практическое исправление — заменить release-сигнализацию на ready.store(true, std::memory_order_release), acquire-чтение флага — на ready.load(std::memory_order_acquire), а оба atomic_signal_fence удалить.
atomic_thread_fence может использоваться в более низкоуровневых схемах, но там необходимо строго доказать связь fence с атомарными операциями и наличие требуемого happens-before. В обычном прикладном коде release/acquire-операции над флагом обычно проще для проверки и менее подвержены ошибкам.
В очереди событий разработчик добавил atomic_signal_fence, чтобы «опубликовать» заполненный буфер перед установкой атомарного флага. На одной архитектуре тесты проходили, но перенос на другую платформу привёл к редким повреждениям читаемых данных.
Рассматривались три варианта. Использование только atomic_signal_fence было дешёвым, но неверным: оно не создавало межпоточную синхронизацию. Явный atomic_thread_fence мог решить задачу, но требовал аккуратной схемы с атомарным флагом и сложнее проверялся.
Выбрали release-запись флага и acquire-чтение флага. Это явно выразило протокол публикации, обеспечило happens-before для содержимого буфера и сохранило возможность использовать обычные, а не атомарные поля внутри уже опубликованного объекта. Цена решения — необходимость не изменять опубликованные данные без отдельной синхронизации.
Нет. atomic_thread_fence не является самостоятельным каналом передачи данных. Для синхронизации должны выполняться условия связи fence с атомарными операциями, а потребитель должен наблюдать соответствующую запись. Без такой связи fence не создаёт нужного отношения happens-before.
Нет. Барьер не устраняет ограничения обработчиков сигналов и не превращает произвольный объект в безопасный для асинхронного доступа. Для связи с обработчиком нужно учитывать специальные правила сигналов, время жизни объектов и допустимые операции; обычно используют подходящие атомарные объекты или volatile std::sig_atomic_t там, где это соответствует задаче.
Только для записей, выполненных до release-публикации, если acquire действительно связан с этой публикацией. Последующие изменения объекта снова требуют синхронизации, единственного владельца или иного протокола доступа. Первичная публикация не превращает объект в неизменяемый и не защищает его от будущих конфликтующих записей.