mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 05:14:34 +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`.
|
||
|
||
С `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
|