Пожалуй, это самый популярный вопрос для собеседования на позицию C++-разработчика.
И не без причины: этим прекрасным умным указателем так просто пользоваться (в сравнении с его собратом — std::unique_ptr
), что легко не заметить подвох. В его названии есть shared
. Да он и спроектирован так, чтобы его можно было разделять между потоками. Что может пойти не так?!
Всё.
Новички довольно быстро обнаруживают первую линию костыльно-грабельной обороны бастиона сложности shared_ptr
: если доступ к самому указателю shared_ptr<T>
«безопасен», то к объекту T
все равно надо синхронизировать.
Это очевидно, это заметно, это понятно. Но дальше ведь все просто?
Нет.
Дальше притаились волчьи ямы с отравленными копьями. Сам объект-указатель shared_ptr
не является потокобезопасным. И доступ к самому указателю тоже надо синхронизировать!
Как же так?! Мы никогда не синхронизировали и у нас все работало.
Поздравляю, у вас одно из двух:
- Либо все доступы к указателю из разных потоков только на чтение. И тогда проблем действительно нет.
- Программа работает по воле случая.
using namespace std::literals::chrono_literals;
std::shared_ptr<std::string> str = nullptr;
std::jthread t1 { [&]{
std::size_t cnt_miss = 0;
while (!str) {
++cnt_miss;
}
std::cout << "count miss: " << cnt_miss << "\n";
std::cout << *str << "\n";
} };
std::jthread t2 { [&] {
std::this_thread::sleep_for(500ms);
str = std::make_shared<std::string>("Hello World");
}
};
Аналогично другим примерам с race condition код выше перестает работать при изменении уровня оптимизации.
Но ведь вы наверняка что-то слышали; все-таки есть в shared_ptr
кое-что потокобезопасное...
Да. Есть. Счетчик ссылок. Больше ничего потокобезопасного в std::shared_ptr
нет.
Атомарный счетчик ссылок как раз и позволяет без проблем копировать один и тот же указатель (увеличивая счетчики) в разные потоки и не синхронизировать вручную вызовы деструкторов (уменьшающих счетчики) в разных потоках.
Если вам надо менять указатель из разных потоков, то вам нужен std::atomic<std::shared_ptr<T>>
(C++20). Либо использовать функции std::atomic_load
/std::atomic_store
и прочие — у них есть специальные перегрузки для shared_ptr
.
С std::weak_ptr
все то же самое.