Files
ubbook/concurrency/shared_ptr.md
2022-02-20 14:42:44 +02:00

55 lines
3.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Потокобезопасен ли `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