Сравните проверку доступности объекта через std::weak_ptr::expired() с попыткой получить std::shared_ptr через lock(): какой способ корректен при возможном уничтожении объекта другим потоком?
При возможном уничтожении объекта другим потоком нужно сразу использовать std::weak_ptr::lock() и проверять возвращённый std::shared_ptr. Связка expired() с последующим получением владельца некорректна: между этими операциями объект может быть уничтожен.
lock() атомарно проверяет, жив ли объект, и при успехе увеличивает число владельцев. Поэтому полученный std::shared_ptr гарантирует жизнь объекта до конца своего времени существования, хотя не обеспечивает потокобезопасность доступа к данным самого объекта.
std::weak_ptr предназначен для наблюдения за объектом, которым владеют через std::shared_ptr, без увеличения числа владельцев. Такой механизм нужен, когда компонент должен ссылаться на объект, но не должен продлевать его жизнь.
Отдельная проблема возникает в многопоточных программах: состояние объекта может измениться между двумя последовательными операциями. Поэтому проверка доступности и получение владения должны выполняться как единая операция, что и предоставляет lock().
Вызов expired() сообщает состояние только на момент проверки. Если он вернул false, другой поток всё ещё может уничтожить последний std::shared_ptr до того, как текущий поток получит собственный владеющий указатель.
В результате попытка создать std::shared_ptr из уже просроченного std::weak_ptr может завершиться исключением std::bad_weak_ptr. Ещё опаснее вариант, при котором после проверки выполняется обращение к объекту через другой невладеющий указатель: объект уже может быть уничтожен, что приводит к неопределённому поведению.
std::weak_ptr хранит ссылку на управляющий блок, но не считается владельцем объекта. Управляющий блок существует, пока жив хотя бы один std::shared_ptr или std::weak_ptr; сам объект уничтожается после исчезновения последнего сильного владельца.
lock() обращается к управляющему блоку и выполняет проверку сильного счётчика с попыткой увеличить его как единую атомарную операцию. Если объект ещё жив, возвращается непустой std::shared_ptr. Если последний владелец уже исчез или одновременно исчезает раньше успешного увеличения счётчика, возвращается пустой std::shared_ptr.
Минимальный пример корректного шаблона:
Вызов lock() не выбрасывает исключение из-за истёкшего объекта: результатом будет пустой указатель. В отличие от этого, конструктор std::shared_ptr из std::weak_ptr при отсутствии объекта выбрасывает std::bad_weak_ptr, поэтому для условного получения владения обычно выбирают именно lock().
Атомарность относится к операциям со счётчиками и управляющим блоком. Она не делает сам объект безопасным для одновременного чтения и изменения: если несколько потоков обращаются к общим данным объекта, нужна отдельная синхронизация, например mutex или иной подход, соответствующий задаче.
После успешного lock() локальный std::shared_ptr должен сохраняться на всё время обращения к объекту. Нельзя получить его, извлечь сырой указатель, уничтожить сильного владельца и продолжить использовать сырой указатель: lock() защищает время жизни только объекта, на который непосредственно ссылается полученный std::shared_ptr.
Кэш хранит объекты через std::shared_ptr, а фоновые задачи получают на них std::weak_ptr. Задача должна обработать объект только в том случае, если он ещё нужен системе, но не должна сама удерживать его в кэше навсегда.
Вариант с expired() и последующим созданием владельца плох: между проверкой и созданием std::shared_ptr другой поток может удалить объект. Вариант с сырым указателем ещё хуже, поскольку не создаёт гарантии времени жизни.
Выбранное решение — вызвать lock() непосредственно перед обработкой и использовать возвращённый std::shared_ptr. Если он пуст, задача завершает работу; если непуст, объект гарантированно жив до конца области действия локального владельца. Это устраняет гонку за время жизни, не добавляя искусственного владения со стороны фоновой задачи.
expired() достаточной проверкой?expired() — это только моментальный снимок состояния управляющего блока. Значение false не резервирует объект и не увеличивает счётчик владельцев. Другой поток может уничтожить последний std::shared_ptr сразу после возврата из expired(), поэтому для получения безопасного владения нужно применять lock().
lock()?Результат определяется атомарной операцией внутри lock(). Либо операция успеет увеличить число сильных владельцев и вернёт непустой std::shared_ptr, либо обнаружит отсутствие объекта и вернёт пустой указатель. Промежуточного состояния, в котором возвращённый std::shared_ptr указывает на уже уничтоженный объект, быть не должно.
lock() безопасное изменение объекта из нескольких потоков?Нет. Успешный lock() гарантирует только время жизни объекта и корректную работу со счётчиками владения. Два потока всё ещё могут одновременно изменить один и тот же объект и создать гонку данных, если его внутреннее состояние не защищено синхронизацией.