Ситуация: поток публикует объект через std::atomic<std::shared_ptr<T>>. Какую гарантию получает читатель относительно самого объекта?
std::atomic<std::shared_ptr<T>> атомарно публикует и считывает владение объектом: читатель, успешно получивший shared_ptr, не даст объекту уничтожиться до окончания своего владения. Но это не делает состояние объекта T потокобезопасным: одновременная запись и чтение его обычных полей по-прежнему могут привести к гонке данных.
Если публикация выполняется с release, а чтение — с acquire, читатель также получает видимость инициализации, выполненной до публикации. Для изменяемого объекта отдельная синхронизация всё равно необходима.
Обычная передача указателя между потоками не решает задачу времени жизни объекта: один поток может удалить объект, пока другой ещё использует его. std::shared_ptr устраняет эту проблему владением через счётчик ссылок, но доступ к одной и той же переменной shared_ptr из разных потоков сам по себе не становится безопасным.
В C++11 появились атомарные свободные функции для операций над shared_ptr, а в C++20 — специализация std::atomic<std::shared_ptr<T>>. Она объединила атомарное изменение владения с обычным интерфейсом атомиков.
Пусть один поток заменяет текущую конфигурацию, а другие потоки получают её для работы. Нужно одновременно решить две разные задачи: безопасно опубликовать новую ссылку и гарантировать, что объект не будет уничтожен во время чтения.
Атомарность указателя не распространяется на поля объекта. Если один поток изменяет object->value, а другой читает это поле без мьютекса, атомика внешнего shared_ptr не устраняет data race и не задаёт согласованный снимок состояния.
Атомарная специализация делает операции над самой переменной shared_ptr неделимыми относительно других операций над этой же атомарной переменной. Читатель либо получает прежнее владение, либо новое, но не промежуточное состояние указателя.
Полученный shared_ptr увеличивает совместное владение объектом. Поэтому удаление объекта не произойдёт, пока локальная копия читателя существует. Это защищает время жизни, но не сериализует доступ к данным T.
Для публикации полностью инициализированного объекта обычно применяют пару release/acquire. Успешная acquire-загрузка, увидевшая значение, опубликованное release-операцией, получает happens-before от предшествующей инициализации.
В примере объект неизменяемый после публикации, поэтому отдельная блокировка для его поля не требуется. Если объект должен изменяться, возможны три основных решения: защищать его мьютексом, использовать атомарные поля или публиковать новые неизменяемые версии.
У std::atomic<std::shared_ptr<T>> есть и практическая цена: копирование и обновление владения могут быть тяжелее обычного атомика указателя, а реализация не обязана быть lock-free. Кроме того, атомарность одной переменной не создаёт транзакционную согласованность между несколькими атомарными shared_ptr.
В сервисе поток конфигурации заменяет объект настроек, а рабочие потоки регулярно его читают. Передача сырого указателя была бы быстрой, но могла привести к использованию освобождённой памяти. Обычный shared_ptr решал время жизни, однако конкурентная запись в одну переменную shared_ptr оставалась небезопасной.
Вариант с глобальным мьютексом прост и позволяет атомарно менять несколько связанных настроек, но добавляет блокировку каждому читателю. Вариант с атомарным сырым указателем дешевле, но требует отдельной схемы безопасного освобождения: например, отложенного удаления или hazard pointers.
Выбран std::atomic<std::shared_ptr<const Config>>: публикация стала безопасной, читатели не блокируются, а неизменяемость конфигурации исключила гонки внутри объекта. Если в будущем потребуется менять отдельные параметры на месте, эту модель нужно пересмотреть, а не считать внешний атомик достаточным.
1. Достаточно ли атомарной загрузки shared_ptr, чтобы увидеть корректно инициализированный объект?
Нет, это зависит от порядка памяти и от того, какое значение прочитано. Если публикация выполнена с release, а загрузка действительно увидела опубликованное значение через acquire, устанавливается happens-before и инициализация становится видимой. При relaxed атомарность владения сохраняется, но синхронизация обычных записей объекта не гарантируется.
2. Защищает ли atomic<shared_ptr<T>> одновременное изменение полей объекта T?
Нет. Атомарной является переменная shared_ptr, а не память, на которую она указывает. Конкурирующие записи и чтения обычного поля без синхронизации образуют гонку данных; для такого состояния нужны мьютекс, атомарные поля или неизменяемая модель с заменой всего объекта.
3. Можно ли безопасно использовать одну и ту же обычную переменную shared_ptr в нескольких потоках, если разные потоки владеют копиями?
Да, разные копии shared_ptr, указывающие на один объект, обычно можно использовать одновременно: внутренний счётчик владения рассчитан на конкурентное управление. Но конкурентный доступ к одной и той же переменной shared_ptr, когда один поток присваивает ей новое значение, а другой читает или присваивает, требует атомарных операций или синхронизации. Общее владение объектом и безопасность самой переменной — разные гарантии.