Разберите последствия публикации указателя через атомарную переменную с порядком relaxed: может ли читатель увидеть указатель, но невалидное состояние объекта?
Да. Порядок relaxed гарантирует атомарность самой операции и согласованный порядок изменений этой атомарной переменной, но не публикует результаты обычных записей в объект. Если читатель получает указатель через relaxed-загрузку и затем обращается к объекту без установленного отношения happens-before, возникает гонка данных и неопределённое поведение.
Для безопасной публикации обычно применяют запись указателя с memory_order_release и чтение с memory_order_acquire. Тогда успешная acquire-загрузка, увидевшая значение, записанное release-операцией, делает предшествующие записи объекта видимыми читателю.
До стандартизации модели памяти в C++11 переносимое описание взаимодействия потоков в C++ было неполным: компилятор и процессор могли переставлять операции, а поведение при гонках не имело единой формальной основы. Модель памяти C++11 ввела атомики, отношения видимости и правила, связывающие оптимизации с наблюдаемым поведением потоков.
Разделение memory order на несколько уровней появилось как компромисс. Программист может выбрать минимально необходимое упорядочивание и не платить за более сильные барьеры там, где синхронизация не требуется.
Пусть один поток сначала заполняет объект, а затем записывает его адрес в атомарную переменную. Другой поток читает адрес и сразу использует объект. Интуитивное предположение «раз адрес уже опубликован, значит объект готов» неверно при relaxed.
Между обычными записями в объект и обычными чтениями из него не появляется отношение happens-before. Поэтому читатель может наблюдать устаревшие значения, а формально одновременный доступ к неторемым данным без синхронизации может стать гонкой данных. Итогом является неопределённое поведение, а не просто гарантированно устаревшее значение.
Атомарность переменной отвечает только на вопрос, не увидит ли поток разорванную запись указателя. Relaxed не устанавливает межпоточное упорядочивание обычных операций вокруг атомарной операции и не создаёт отношения synchronizes-with.
При схеме release/acquire запись с release публикует все операции, предшествующие ей в том же потоке. Если другой поток выполняет acquire-загрузку и получает значение, записанное этой release-операцией, то опубликованные записи становятся предшествующими чтениям после acquire в модели памяти C++.
Минимальный пример безопасной публикации неизменяемого объекта:
Инициализация data предшествует release-записи, а чтение value следует за acquire-загрузкой, увидевшей опубликованный указатель. В реальном коде дополнительно нужно обеспечить время жизни объекта: атомарная публикация указателя сама по себе не защищает от удаления объекта другим потоком.
Seq_cst даёт более сильное глобальное упорядочивание атомарных операций и иногда упрощает рассуждения, но может быть дороже и не устраняет ошибки времени жизни или доступов к другим незащищённым данным. Relaxed уместен для счётчиков, статистики и других случаев, где не требуется публиковать состояние обычной памяти.
Сервис публиковал указатель на новую конфигурацию через атомарную переменную с relaxed-записью. Потоки-читатели корректно получали ненулевой адрес, но при высокой нагрузке иногда использовали комбинацию старых и новых полей конфигурации; диагностика указывала на редкие падения в коде, который считал конфигурацию полностью инициализированной.
Рассматривались три варианта. Мьютекс был самым простым для доказательства корректности, но добавлял блокировки на каждом чтении. Сохранение relaxed не решало проблему публикации. Переход на release/acquire устранял проблему видимости, но требовал отдельно решить безопасное удаление старых объектов.
Выбрали release/acquire для публикации и схему управления временем жизни через неизменяемые снимки с безопасным для проекта владением объектами. Это разделило две задачи: атомарный обмен ссылкой обеспечивал публикацию, а механизм владения предотвращал обращение к уже уничтоженной конфигурации.
1. Достаточно ли acquire-загрузки, если запись указателя тоже выполнена с relaxed?
Нет. Acquire-загрузка сама по себе не публикует записи писателя. Для появления нужного synchronizes-with должна существовать release-операция, значение которой acquire-загрузка наблюдает напрямую или через допустимую цепочку release-последовательности. Пара relaxed-store и acquire-load не заменяет release/acquire-пару.
2. Делает ли release/acquire безопасным последующее изменение опубликованного объекта?
Нет. Эта пара безопасно публикует уже выполненные записи, но не синхронизирует последующие изменения. Если один поток продолжает менять обычные поля, а другой читает их без дополнительной синхронизации, возникает гонка данных. Объект после публикации должен быть неизменяемым либо защищаться мьютексом, атомарными полями или иной корректной схемой.
3. Защищает ли атомарный указатель от use-after-free?
Нет. Атомарность гарантирует свойства доступа к самой переменной-указателю, но не продлевает время жизни объекта. Поток может атомарно получить адрес, после чего другой поток удалит объект. Для решения нужны владение, например совместный атомарный указатель при поддержке конкретного стандарта и библиотеки, либо hazard pointers, эпохальная reclamation или другая подходящая схема управления памятью.