Разберите ситуацию: несколько потоков читают и заменяют общий std::shared_ptr. Какой механизм C++20 нужен, чтобы синхронизировать доступ к самой переменной-указателю без внешнего мьютекса?
Используйте специализацию std::atomic<std::shared_ptr<T>>. Она делает атомарными операции чтения, записи и обмена значением самой переменной shared_ptr, включая корректное управление её контрольным блоком.
Это не делает объект T потокобезопасным: если потоки изменяют один и тот же объект, для его состояния всё равно нужна отдельная синхронизация либо неизменяемые снимки.
Обычный std::shared_ptr уже допускает одновременную работу разных экземпляров, владеющих одним объектом и общим контрольным блоком. Однако одновременное чтение и изменение одной переменной shared_ptr без синхронизации создаёт гонку данных.
В C++11 для таких операций появились специализированные свободные функции атомарной работы с shared_ptr. В C++20 их более идиоматичной заменой стала специализация std::atomic<std::shared_ptr<T>>, предоставляющая привычный атомарный интерфейс.
Счётчик ссылок внутри контрольного блока и сама переменная shared_ptr — разные сущности. Потокобезопасность операций над контрольным блоком не означает, что два потока могут без синхронизации одновременно читать и присваивать одну переменную shared_ptr.
Такая ошибка является неопределённым поведением: один поток может читать внутреннее состояние указателя в момент, когда другой его изменяет. Кроме того, простая защита указателя не синхронизирует поля объекта, на который он указывает.
Атомарный shared_ptr хранит владение так, чтобы операции load, store, exchange и сравнение с обменом выполнялись как единая синхронизированная операция над переменной.
Читатель получает собственную копию shared_ptr через load. Поэтому выбранный объект и его контрольный блок остаются живы до окончания работы читателя, даже если другой поток уже заменил значение в атомарной переменной.
Обычно такой подход сочетают с неизменяемыми объектами: поток публикует новый снимок конфигурации, а читатели безопасно используют ранее загруженные снимки. Если объект изменяемый, атомарность указателя не защищает его поля — потребуются мьютекс, атомики внутри объекта или иной протокол доступа.
Атомарность не означает обязательную аппаратную lock-free реализацию. Проверить это можно через is_lock_free; конкретная реализация вправе использовать внутреннюю блокировку. Если требуется совместимость со стандартами до C++20, применяют специализированные свободные функции std::atomic_load и std::atomic_store для shared_ptr.
При атомарной замене увеличение счётчика владения, необходимое для нового значения, входит в атомарную операцию. Уменьшение счётчика для старого значения может быть завершено уже после неё, поэтому пользовательский деструктор не следует считать выполняющимся строго внутри атомарной операции.
Сервис периодически обновляет конфигурацию, а сотни рабочих потоков читают её. Вариант с обычным shared_ptr без синхронизации имеет гонку данных. Вариант с мьютексом корректен, но каждый читатель конкурирует за блокировку и может задерживать обработку запросов.
Можно защищать изменяемую конфигурацию мьютексом, но это увеличивает стоимость чтения и усложняет настройку времени блокировок. Более подходящий вариант — std::atomic<std::shared_ptr<const Config>>: обновляющий поток создаёт новый неизменяемый снимок и публикует его, а читатели атомарно получают стабильные снимки без общей блокировки.
В результате читатели не видят частично обновлённую конфигурацию, старые снимки автоматически освобождаются после завершения их использования, а синхронизация ограничена заменой указателя. Цена решения — дополнительные выделения памяти при создании снимков и отсутствие гарантии lock-free.
shared_ptr, если два потока изменяют поля объекта T?Нет. Атомарной является переменная, содержащая shared_ptr, а не объект T. Для совместного изменения объекта нужны мьютекс, атомарные поля или другой механизм синхронизации; часто проще публиковать новые неизменяемые экземпляры.
shared_ptr обычно безопасно при общем объекте?Разные экземпляры могут одновременно обращаться к одному контрольному блоку: операции управления счётчиком владения синхронизированы реализацией. Но это не распространяется на одну и ту же переменную shared_ptr, если один поток её читает, а другой присваивает или сбрасывает.
std::atomic<std::shared_ptr<T>> отсутствие блокировок?Нет. Стандарт определяет атомарную семантику, но не требует lock-free реализации. Внутри может использоваться блокировка, поэтому при жёстких требованиях к задержкам нужно отдельно проверить is_lock_free и измерить поведение на целевой платформе.