std :: shared_ptr конструктор копирования безопасности потока - PullRequest
0 голосов
/ 20 января 2019

Спецификация std :: shared_ptr гарантирует, что только один поток вызовет удаление внутреннего указателя.Этот ответ содержит действительно приятное объяснение о порядке упорядочения памяти при манипулировании счетчиком refrence для shared_ptr, чтобы гарантировать, что удаление будет вызвано в синхронизированной памяти.

Что я не делаюпонимать следующее:

  • Если shared_ptr инициализируется конструктором копирования, гарантируется ли, что он будет пустым или действительным shared_ptr?

Я ищув MVCC реализация конструктора копирования shared_ptr.и я думаю, что смогу идентифицировать хотя бы одно состояние гонки.

template<class _Ty2>
    void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
    {   // implement shared_ptr's (converting) copy ctor
    if (_Other._Rep)
        {
        _Other._Rep->_Incref();
        }

    _Ptr = _Other._Ptr;
    _Rep = _Other._Rep;
    }

Реализация проверяет, что блок управления является действительным, затем увеличивает его счетчик ссылок и копирует, присваивает внутренние поля.

Предполагая, что _Other принадлежит другому потоку, отличному от того, который вызывает конструктор копирования.Если между строками if (_Other._Rep) и _Other._Rep->_Incref(); этот поток вызывает деструктор, который удаляет блок управления и указатель, то _Other._Rep->_Incref() разыменует удаленный указатель.

Дальнейшее уточнение

Вот код, который иллюстрирует угловой случай, о котором я говорю.Я настрою реализацию конструктора копирования share_ptr для имитации переключения контекста:

template<class _Ty2>
    void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
    {   // implement shared_ptr's (converting) copy ctor
    if (_Other._Rep)
        {
        // now lets put here a really long loop or sleep to simulate a context switch
        int count = 0;
        for (int i = 0; i < 99999999; ++i)
        {           
            for (int j = 0; j < 99999999; ++j)
            {
              count++;
           }
        }

        // by the time we get here, the owning thread may already destroy the  shared_ptr that was passed to this constructor
        _Other._Rep->_Incref();
        }

    _Ptr = _Other._Ptr;
    _Rep = _Other._Rep;
    }

А вот код, который, вероятно, покажет проблему:

int main()
{
    {
        std::shared_ptr<int> sh1 = std::make_shared<int>(123);
        auto lambda = [&]()
        {
            auto sh2 = sh1;
            std::cout << sh2.use_count(); // this prints garbage, -572662306 in my case
        };

        std::thread t1(lambda);
        t1.detach();
        // main thread destroys the shared_ptr
        // background thread probably did not yet finished executing the copy constructor
    }



    Sleep(10000);

}

Ответы [ 2 ]

0 голосов
/ 21 января 2019

Управление состоянием, общим для shared_ptr объектов, является поточно-ориентированным; shared_ptr само по себе не является поточно-ориентированным. Вы не можете манипулировать одним и тем же объектом shared_ptr из разных потоков одновременно; попытка сделать это - гонка данных и, следовательно, UB.

Так что ваш код был бы в порядке, если бы lambda скопировал указатель перед отправкой в ​​другой поток.

Следует также отметить, что ваш конкретный пример не может никогда работать, независимо от того, как написано shared_ptr. Тип может быть atomic<int>, и он все равно будет таким же сломанным. Вы дали лямбде ссылку на объект, который может не существовать до того, как лямбда выполнит операцию копирования.

Никакая внутренняя защита нитей не сможет вас там спасти. Передача ссылки на переменную стека в другой поток всегда должна рассматриваться как запах кода.

0 голосов
/ 20 января 2019

Когда shared_ptr используется должным образом , то, что вы описываете, никогда не произойдет.

Копируемый shared_ptr увеличил refcount перед передачей в конструктор копированияи он не может быть уничтожен до тех пор, пока не выйдет конструктор копирования, так как он является локальным параметром конструктора.

Таким образом, другой поток не уничтожит объект, к которому предоставлен общий доступ.Счетчик _Other.Rep всегда будет по крайней мере 1 при входе в конструктор копирования, если _Other.Rep не равен нулю.

ОБНОВЛЕНИЕ : ваш вариант использования неверен.Лямбда захватывает ссылку на экземпляр shared_ptr основного потока, но поток не создает копию этого shared_ptr до тех пор, пока он не выйдет из области видимости и не будет уничтожен main.В вашей теме есть свисающая ссылка , в результате чего ваш код имеет неопределенное поведение .Это не ошибка реализации shared_ptr.Ваша лямбда-функция должна захватывать shared_ptr по значению вместо по ссылке , поэтому ее счетчик увеличивается немедленно, до создания потока, а не при запуске потока.

...