Skip to content

Latest commit

 

History

History
54 lines (38 loc) · 3.82 KB

shared_ptr.md

File metadata and controls

54 lines (38 loc) · 3.82 KB

Потокобезопасен ли std::shared_ptr?

Пожалуй, это самый популярный вопрос для собеседования на позицию C++-разработчика. И не без причины: этим прекрасным умным указателем так просто пользоваться (в сравнении с его собратом — std::unique_ptr), что легко не заметить подвох. В его названии есть shared. Да он и спроектирован так, чтобы его можно было разделять между потоками. Что может пойти не так?!

Всё.

Новички довольно быстро обнаруживают первую линию костыльно-грабельной обороны бастиона сложности shared_ptr: если доступ к самому указателю shared_ptr<T> «безопасен», то к объекту T все равно надо синхронизировать. Это очевидно, это заметно, это понятно. Но дальше ведь все просто?

Нет.

Дальше притаились волчьи ямы с отравленными копьями. Сам объект-указатель shared_ptr не является потокобезопасным. И доступ к самому указателю тоже надо синхронизировать!

Как же так?! Мы никогда не синхронизировали и у нас все работало.

Поздравляю, у вас одно из двух:

  1. Либо все доступы к указателю из разных потоков только на чтение. И тогда проблем действительно нет.
  2. Программа работает по воле случая.
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 все то же самое.

Полезные ссылки

  1. https://stackoverflow.com/questions/9127816/stdshared-ptr-thread-safety-explained
  2. https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2