简体   繁体   English

std :: shared_ptr复制构造函数线程安全

[英]std::shared_ptr copy constructor thread safety

std::shared_ptr specification guarentees that only one thread will invoke delete on the internal pointer. std :: shared_ptr规范保证只有一个线程将在内部指针上调用delete。 This answer has a really nice explanation about the required memory ordering on the shared_ptr refrence count manipulation in order to guarentee that the delete will be invoked on a synchronized memory. 这个答案对shared_ptr刷新计数操作上所需的内存顺序进行了很好的解释,以确保删除将在同步的内存上进行。

What I don't understand is the following: 我不明白的是以下内容:

  • If a shared_ptr is initialized by a copy constructor, is it guarenteed that it will either be empty or a valid shared_ptr? 如果shared_ptr由副本构造函数初始化,是否可以保证它为空或有效的shared_ptr?

I am looking at MVCC implementation of shared_ptr copy constructor. 我正在看shared_ptr复制构造函数的MVCC实现。 and I think I can identify at least one race condition. 而且我认为我至少可以确定一种比赛条件。

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;
    }

The implementation checks that the control block is valid, then incerement its reference count, and copy assigns the internal fields. 该实现检查控制块是否有效,然后确定其引用计数,并复制分配内部字段。

Assuming _Other is owned by a different thread then the one calling the copy constructor. 假设_Other由一个不同的线程拥有,然后调用副本构造函数。 If between the lines if (_Other._Rep) and _Other._Rep->_Incref(); 如果在两行之间if (_Other._Rep)_Other._Rep->_Incref(); this thread calls the destructor that happens to delete the control block and the pointer, then _Other._Rep->_Incref() will dereference a deleted pointer. 该线程调用恰好删除控制块和指针的析构函数,然后_Other._Rep->_Incref()将取消引用已删除的指针。

Further Clarification 进一步澄清

Here is a code that illustrates the corner case I am talking about. 这是一个代码,说明了我正在谈论的特殊情况。 I will tweak share_ptr copy constructor implementation to simulate a context switch: 我将调整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;
    }

And here is a code that will probably show the problem: 这是一个可能会显示问题的代码:

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);

}

When shared_ptr is used properly , what your describe can never happen. 如果正确使用了shared_ptr ,描述将永远不会发生。

The shared_ptr that is being copied from has incremented the refcount before being passed to the copy constructor, and it can't be destructed until after the copy constructor has exited since it is a local parameter of the constructor. 要从中复制的shared_ptr在传递给复制构造函数之前已使refcount递增,并且直到复制构造函数退出后才能对其进行销毁,因为它是构造函数的局部参数。

Thus, another thread will not destroy the object that is being shared. 因此,另一个线程不会破坏正在共享的对象。 The refcount of _Other.Rep will always be at least 1 upon entry into the copy constructor if _Other.Rep is not null. 的引用次数_Other.Rep永远是至少1在进入拷贝构造函数,如果_Other.Rep不为空。

UPDATE : your use case is faulty. 更新 :您的用例是错误的。 The lambda captures a reference to the main thread's shared_ptr instance but the thread does not make a copy of that shared_ptr until after it has already gone out of scope and been destroyed by main . lambda捕获对主线程的shared_ptr实例的引用 ,但该线程不会复制该shared_ptr直到它已经超出范围并被main破坏为止。 Your thread has a dangling reference causing your code to have undefined behavior . 您的线程有一个悬空的引用,导致您的代码具有未定义的行为 That is not a fault of the shared_ptr implementation. 这不是shared_ptr实现的错误。 Your lambda needs to capture the shared_ptr by value instead of by reference instead, so its refcount is incremented immediately, before the thread is created, not when the thread starts running. 您的lambda需要通过值而不是通过引用来捕获shared_ptr ,因此其refcount在创建线程之前立即增加,而不是在线程开始运行时增加。

Manipulation of the state shared by shared_ptr objects is thread-safe; shared_ptr对象共享的状态的操作是线程安全的; shared_ptr itself is not thread-safe. shared_ptr 本身不是线程安全的。 You cannot manipulate the same shared_ptr object from different threads at the same time; 您不能同时在不同线程中操作同一个shared_ptr对象。 attempting to do so is a data race and thus UB. 尝试这样做是为了进行数据竞赛,因此是UB。

So your code would be fine if lambda copied the pointer before being shipped off to another thread. 因此,如果lambda在将指针运送到另一个线程之前复制了指针,则您的代码会很好。

It should also be noted that your specific example could never work, no matter how shared_ptr was written. 还应注意,无论如何编写shared_ptr ,您的特定示例都将无法正常工作。 The type could be atomic<int> and it would still be just as broken. 类型可以是atomic<int> ,并且仍然会损坏。 You gave the lambda a reference to an object that may not exist before the lambda gets to execute the copy operation. 您为lambda提供了对可能在lambda执行复制操作之前可能不存在的对象的引用。

No amount of internal thread-safety can save you there. 内部线程安全无处可挡。 Passing a reference to a stack variable to another thread should always be looked on as a code smell. 将对堆栈变量的引用传递给另一个线程时,应始终将其视为代码气味。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM