mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-16 13:17:18 +03:00
55 lines
3.8 KiB
Markdown
55 lines
3.8 KiB
Markdown
# Потобезопасен ли `std::shared_ptr`?
|
|
|
|
Пожалуй, это самый популярный вопрос для собеседования на позицию C++-разработчика.
|
|
И не без причины: этим прекрасным умным указателем так просто пользоваться (в сравнении с его собратом — `std::unique_ptr`), что легко не заметить подвох. В его названии есть `shared`. Да он и спроектирован так, чтобы его можно было разделять между потоками. Что может пойти не так?!
|
|
|
|
Всё.
|
|
|
|
Новички довольно быстро обнаруживают первую линию костыльно-грабельной обороны бастиона сложности `shared_ptr`: если доступ к самому указателю `shared_ptr<T>` «безопасен», то к объекту `T` все равно надо синхронизировать.
|
|
Это очевидно, это заметно, это понятно. Но дальше ведь все просто?
|
|
|
|
Нет.
|
|
|
|
Дальше притаились волчьи ямы с отравленными копьями. Сам объект-указатель `shared_ptr` не является потокобезопасным. И доступ к самому указателю тоже надо синхронизировать!
|
|
|
|
Как же так?! Мы никогда не синхронизировали и у нас все работало.
|
|
|
|
Поздравляю, у вас одно из двух:
|
|
1. Либо все доступы к указателю из разных потоков только на чтение. И тогда проблем действительно нет.
|
|
2. Программа работает по воле случая.
|
|
|
|
```C++
|
|
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](./race_condition.md) код выше [перестает](https://godbolt.org/z/zocsYo) работать при изменении уровня оптимизации.
|
|
|
|
Но ведь вы наверняка что-то слышали; все-таки есть в `shared_ptr` кое-что потокобезопасное...
|
|
|
|
Да. Есть. Счетчик ссылок. Больше ничего потокобезопасного в `std::shared_ptr` нет.
|
|
Атомарный счетчик ссылок как раз и позволяет без проблем копировать один и тот же указатель (увеличивая счетчики) в разные потоки и не синхронизировать вручную вызовы деструкторов (уменьшающих счетчики) в разных потоках.
|
|
|
|
Если вам надо менять указатель из разных потоков, то вам нужен `std::atomic<std::shared_ptr<T>>` (C++20). Либо использовать функции ` std::atomic_load`/`std::atomic_store` и прочие — у них есть специальные перегрузки для `shared_ptr`.
|
|
|
|
C `std::weak_ptr` все то же самое.
|
|
|
|
## Полезные ссылки
|
|
1. https://stackoverflow.com/questions/9127816/stdshared-ptr-thread-safety-explained
|
|
2. https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2
|