Как доступы `weak_ptr` и `shared_ptr` атомарны

std::shared_ptr<int> int_ptr;

int main() {
    int_ptr = std::make_shared<int>(1);
    std::thread th{[&]() {
        std::weak_ptr int_ptr_weak = int_ptr;
        auto int_ptr_local = int_ptr_weak.lock();
        if (int_ptr_local) {
            cout << "Value in the shared_ptr is " << *int_ptr_local << endl;
        }
    });

    int_ptr.reset(nullptr);
    th.join();
    return 0;
}

Является ли приведенный выше код потокобезопасным? Я прочитал этот ответ О потокобезопасности weak_ptr, но просто хотел убедиться что приведенный выше код является потокобезопасным.

Причина, по которой я спрашиваю об этом, заключается в том, что если приведенный выше код действительно является потокобезопасным, я не могу понять, как интерфейсы std::weak_ptr и std::shared_ptr делают следующую операцию атомарной expired() ? shared_ptr<T>() : shared_ptr<T>(*this). Мне просто кажется, что создание двух логических строк кода, как показано выше, невозможно сделать синхронным без использования какого-либо мьютекса или спин-блокировки.

Я понимаю, как атомарные приращения работают с разными экземплярами общих указателей, и я понимаю, что shared_ptr сами по себе не являются потокобезопасными, но если приведенное выше действительно является потокобезопасным, это очень похоже на потокобезопасное shared_ptr, и я не понимаю, как две строки код, как в условном выше, можно сделать атомарным без блокировок.


person Curious    schedule 06.01.2017    source источник
comment
это очень похоже на потокобезопасность shared_ptr. Является ли использование shared-shared_ptr для создания weak_ptr действительно потокобезопасным?   -  person Holt    schedule 06.01.2017
comment
Ничто не мешает реализации использовать мьютекс, чтобы сделать lock атомарным, за исключением того, что это было бы не очень эффективно.   -  person Oktalist    schedule 06.01.2017


Ответы (4)


Этот вопрос состоит из двух частей:

Потокобезопасность

Код НЕ ориентирован на многопотоковое исполнение, но это не имеет ничего общего с lock():
существует гонка между int_ptr.reset(); и std::weak_ptr int_ptr_weak = int_ptr;. Потому что один поток модифицирует неатомарную переменную int_ptr, а другой читает ее, что по определению является гонкой данных.

Так что это было бы нормально:

int main() {
    auto int_ptr = std::make_shared<int>(1);
    std::weak_ptr<int> int_ptr_weak = int_ptr;  //create the weak pointer in the original thread
    std::thread th( [&]() {
        auto int_ptr_local = int_ptr_weak.lock();
        if (int_ptr_local) {
            std::cout << "Value in the shared_ptr is " << *int_ptr_local << std::endl;
        }
    });

    int_ptr.reset();
    th.join();
}

Атомная версия примера кода expired() ? shared_ptr<T>() : shared_ptr<T>(*this)

Конечно, весь процесс не может быть атомарным. На самом деле важная часть заключается в том, что счетчик сильных ссылок увеличивается только в том случае, если он уже больше нуля, и что проверка и приращение происходят атомарным образом. Я не знаю, есть ли для этого какие-либо специфичные для системы/архитектуры примитивы, но один из способов реализовать это в С++ 11:

std::shared_ptr<T> lock() {
    if (!isInitialized) {
        return std::shared_ptr<T>();
    }
    std::atomic<int>& strong_ref_cnt = get_strong_ref_cnt_var_from_control_block();
    int old_cnt = strong_ref_cnt.load();
    while (old_cnt && !strong_ref_cnt.compare_exchange_weak(old_cnt, old_cnt + 1)) {
        ;
    }
    if (old_cnt > 0) {
        // create shared_ptr without touching the control block any further
    } else {
        // create empty shared_ptr
    }
}
person MikeMB    schedule 07.01.2017
comment
Спасибо за ответ! Единственная часть, которая меня смутила, заключалась в том, что способ выполнения lock() включает в себя блокировку (в вашем примере спин-блокировку). Я исходил из того, что стандарт говорит, что функция lock() будет выполняться без блокировки. Разве это не правда? - person Curious; 07.01.2017
comment
@Curious: Откуда у вас сложилось впечатление, что операция должна быть без блокировки? Стандарт только говорит, что это должно происходить атомарно, что не то же самое, что без блокировки. На самом деле операции над std::atomic_flag — это единственные операции, которые согласно стандарту должны быть безблокировочными. Также я не уверен, назову ли я это спин-блокировкой. так как код здесь ничего не получает, не выпускает и не ждет - person MikeMB; 08.01.2017
comment
@Curious в вашем примере спин-блокировка Нет спин-блокировки. - person curiousguy; 05.08.2018

Является ли приведенный выше код потокобезопасным?

Я думаю, что нет, так как int_ptr.reset(nullptr); соревнуется с std::weak_ptr int_ptr_weak = int_ptr;.

Я не могу понять, как интерфейсы std::weak_ptr и std::shared_ptr делают следующую операцию атомарной expired() ? shared_ptr<T>() : shared_ptr<T>(*this)

Такая операция не является атомарной, поскольку expired() может возвращать значение false, но к тому времени, когда вы воздействуете на это значение, оно уже может быть неточным. С другой стороны, если он возвращает значение true, оно гарантированно останется точным, если с тех пор никто не модифицировал конкретный экземпляр shared_ptr. То есть операции с другими копиями данного shared_ptr не могут привести к истечению срока его действия.

Реализация weak_ptr::lock() не будет использовать expired(). Вероятно, он будет делать что-то вроде атомарного сравнения-обмена, когда дополнительная сильная ссылка добавляется только в том случае, если текущее количество сильных ссылок больше нуля.

person Joseph Artsimovich    schedule 06.01.2017
comment
Я спросил, потому что cppreference говорит, что строка кода атомарна. en.cppreference.com/w/cpp/memory/weak_ptr/lockЯ неправильно это истолковал? - person Curious; 06.01.2017
comment
Формулировка на cppreference.com выглядит следующим образом: Эффективно возвращает expired() ? shared_ptr‹T›() : shared_ptr‹T›(*this), выполняется атомарно. Это следует интерпретировать как нечто подобное, но атомарно. - person Joseph Artsimovich; 06.01.2017
comment
Ваше использование weak_ptr::lock() действительно потокобезопасно, однако существует несвязанное состояние гонки между созданием слабого указателя из сильного и сбросом этого сильного указателя. - person Joseph Artsimovich; 06.01.2017
comment
@Curious: Да, поэтому последние два слова выполняются атомарно. Фрагмент кода описывает только результат, а не фактический процесс его достижения. - person MSalters; 06.01.2017
comment
@Tulon, почему тогда говорится, что операция lock () является потокобезопасной? относительно чего он безопасен? - person Curious; 06.01.2017
comment
@Curious: операция weak_ptr::lock() безопасна по отношению к параллельным операциям с другими экземплярами shared_ptr и weak_ptr, которые разделяют состояние с weak_ptr, для которого вы вызываете lock(). К таким операциям относится и уничтожение другого экземпляра. - person Joseph Artsimovich; 06.01.2017
comment
@Curious: Кстати, чтобы сделать ваш код действительно потокобезопасным, вам нужно инициализировать weak_ptr в основном потоке, а затем захватить его в поток по значению, а не по ссылке. - person Joseph Artsimovich; 06.01.2017
comment
@Tulon, не могли бы вы объяснить, что ответ пытался передать в сообщении, на которое я ссылался? - person Curious; 06.01.2017
comment
@Curious: Вы имеете в виду этот ответ? Он просто повторяет, что одновременный доступ к разным экземплярам shared_ptr или weak_ptr, которые имеют одно и то же состояние, — это нормально, а одновременный доступ к одному и тому же экземпляру (если только это не доступ только для чтения) — нет. - person Joseph Artsimovich; 06.01.2017
comment
Но он говорит, что вы можете использовать weak_ptr::lock() для получения shared_ptr из других потоков без дальнейшей синхронизации. Разве это не означает, что вы можете получить weak_ptr из shared_ptr, содержащихся в других потоках, без каких-либо блокировок? - person Curious; 06.01.2017
comment
@Curious: weak_ptr::lock() - это константный метод, поэтому он считается доступом только для чтения. Вы можете вызывать константные методы для одного и того же экземпляра weak_ptr одновременно из нескольких потоков, если ни один другой поток не вызывает неконстантные методы для этого экземпляра. Тем не менее, рекомендуется хранить частную копию weak_ptr, к которой поток будет иметь монопольный доступ. Обратите внимание, что константные методы, являющиеся потокобезопасными, применяются не только к любому коду, но и к коду в стандартной библиотеке. - person Joseph Artsimovich; 06.01.2017
comment
@Curious Поток не содержит объектов. Вы имеете в виду автоматическую переменную функций, работающих в других потоках? - person curiousguy; 05.08.2018

Нет, ваш код не является потокобезопасным. Существует гонка данных между операцией int_ptr.reset() в основном потоке (которая является операцией записи) и инициализацией int_weak_ptr из int_ptr в th (которая является операцией чтения).

person Wil Evers    schedule 06.01.2017
comment
Но cppreference, кажется, говорит, что метод lock выполняет свою логику атомарно en.cppreference. com/w/cpp/memory/weak_ptr/lock - person Curious; 06.01.2017
comment
@Curious В ответе не упоминается lock. - person Oktalist; 06.01.2017

" как интерфейсы std::weak_ptr и std::shared_ptr делают следующую операцию атомарной expired() ? shared_ptr<T>() : shared_ptr<T>(*this)"

Интерфейсы - нет. Это внутреннее свойство реализации. Как именно это делается, зависит от реализации.

person MSalters    schedule 06.01.2017
comment
Я просто не понял, как эта строка кода может быть атомарной без блокировок. Не могли бы вы привести пример любой такой реализации? - person Curious; 06.01.2017
comment
@Curious: эта строка кода не атомарна. Реализации будут иметь другой код. Поскольку это шаблон, вы можете просто найти пример в своей реализации. - person MSalters; 06.01.2017