В старом интерфейсе поле нельзя заменить на std::atomic без изменения ABI. Что нужно обеспечить, чтобы безопасно использовать его через std::atomic_ref?
Нужно, чтобы объект имел подходящий тип и выравнивание, жил дольше всех связанных с ним объектов std::atomic_ref, а доступ к нему в этот период выполнялся только через std::atomic_ref. Само создание atomic_ref не делает безопасными параллельные обычные чтения и записи того же объекта.
std::atomic_ref появился в C++20 для атомарного доступа к уже существующему объекту без замены его типа на std::atomic. Это полезно, когда изменение структуры объекта затрагивает ABI, формат данных, размещение в общей памяти или внешний интерфейс.
Подход решает задачу добавления атомарных операций на уровне доступа, но не изменяет свойства самого объекта: он не превращает объект в универсально безопасный для любых способов обращения.
Пусть несколько потоков обращаются к обычному полю через разные экземпляры std::atomic_ref. Это допустимо: такие экземпляры могут совместно атомарно работать с одним объектом.
Опасная ситуация возникает, если один поток использует atomic_ref, а другой одновременно читает или изменяет поле обычным способом. Даже если аппаратная операция кажется атомарной, смешение атомарного и неатомарного доступа к одному объекту может привести к гонке данных и неопределённому поведению.
Также нельзя допускать, чтобы объект был уничтожен раньше любого atomic_ref, ссылающегося на него. Выравнивание объекта должно удовлетворять требованиям реализации для std::atomic_ref<T>; недостаточное выравнивание делает использование некорректным.
Тип T должен удовлетворять требованиям atomic_ref, в частности быть тривиально копируемым. Все конкурентные обращения к объекту в период существования связанных atomic_ref должны выполняться через них.
Разные экземпляры std::atomic_ref, указывающие на один объект, работают с одной атомарной переменной: для операций существует единый порядок модификаций этого объекта. Порядок памяти выбирается так же, как для std::atomic: relaxed подходит для простого атомарного счётчика, acquire/release или seq_cst нужны, когда операции также устанавливают межпоточное отношение видимости.
Минимальный пример атомарного счётчика на существующем объекте:
Здесь оба потока атомарно изменяют один int, хотя сам counter не объявлен как std::atomic. После join обычное чтение counter допустимо: завершение потока и join создают необходимую синхронизацию, а все конкурентные изменения уже закончились.
std::atomic_ref не гарантирует отсутствие блокировок. Возможность lock-free нужно проверять через is_lock_free; для производительности также важно учитывать ложное совместное использование кэш-линий и размещение объекта в памяти.
В бинарном интерфейсе библиотеки уже существует поле-счётчик типа int. Замена его на std::atomic<int> может изменить требования к выравниванию, размеру или представлению структуры и нарушить совместимость с клиентами.
Рассматривались два варианта. Защита внешним мьютексом проще и позволяет атомарно обновлять несколько связанных полей, но добавляет блокировки и требует дисциплины во всех вызывающих местах. Изменение ABI на std::atomic даёт явную семантику, однако может быть неприемлемо для уже выпущенного интерфейса.
Выбран std::atomic_ref с подходящим выравниванием. Все пути конкурентного доступа перевели на него, время жизни ссылок ограничили временем жизни поля, а для счётчика использовали relaxed. Это сохранило представление существующего объекта и устранило гонку; для публикации других данных потребовались бы отдельные acquire/release-операции.
Нет. Atomic_ref не устанавливает режим, в котором любые обращения к объекту автоматически становятся атомарными. Все конкурентные доступы должны проходить через atomic_ref либо через другой согласованный механизм синхронизации; обычный доступ, выполняемый одновременно с атомарным, может образовать гонку данных.
Только если не осталось ни одного живого atomic_ref, ссылающегося на объект. Требование относится к времени жизни всех экземпляров atomic_ref, а не только к моменту последней атомарной операции. Поэтому ссылки обычно создают локально и не сохраняют дольше защищаемого объекта.
Нет. Atomic_ref предоставляет атомарность и выбранный порядок памяти, но не обязан обеспечивать lock-free прогресс. Реализация может использовать внутреннюю блокировку, поэтому при жёстких требованиях к задержкам следует проверять is_lock_free и оценивать архитектуру целевой платформы.