【问题标题】:std::shared_ptr copy constructor thread safetystd::shared_ptr 复制构造函数线程安全
【发布时间】:2019-01-20 18:55:40
【问题描述】:

std::shared_ptr 规范保证只有一个线程会在内部指针上调用 delete。 这个answer 对 shared_ptr 引用计数操作所需的内存排序有一个非常好的解释,以保证删除将在同步内存上调用。

我不明白的是:

  • 如果 shared_ptr 由复制构造函数初始化,是 保证它将为空或有效的 shared_ptr?

我正在查看 shared_ptr 复制构造函数的 MVCC 实现。我想我至少可以识别出一种竞争条件。

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-&gt;_Incref(); 行之间,该线程调用了恰好删除控制块和指针的析构函数,那么_Other._Rep-&gt;_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);

}

【问题讨论】:

  • 您是否建议可以在一个线程中复制一个对象,而在另一个线程中销毁同一对象?
  • 按照这个标准,互斥锁也不是“线程安全的”...
  • "现在让我们在这里放一个很长的循环或休眠来模拟上下文切换" int count = 0; 让它volatile:编译器只期望产生相同的结果程序的可观察行为,即与外部世界的交互与 C++ 规范所描述的虚拟机执行的交互相同。 空循环根本没有可观察的行为,可以将其删除。 根据定义,对volatile 限定对象的访问是可观察的,因此无法删除访问此类对象的循环。

标签: c++ multithreading thread-safety shared-ptr smart-pointers


【解决方案1】:

如果使用shared_ptr正确,您的描述永远不会发生。

正在从中复制的shared_ptr 在传递给复制构造函数之前增加了引用计数,并且由于它是构造函数的本地参数,因此直到复制构造函数退出后才能销毁它。

因此,另一个线程不会破坏正在共享的对象。如果_Other.Rep 不为null,则在进入复制构造函数时_Other.Rep 的引用计数将始终至少为1。

更新:您的用例有问题。 lambda 捕获对主线程的shared_ptr 实例的引用,但线程不会复制该shared_ptr,直到它已经超出范围并被main 销毁。您的线程有一个悬空引用,导致您的代码有未定义的行为。这不是shared_ptr 实现的错误。您的 lambda 需要捕获 shared_ptr by value 而不是 by reference,因此它的 refcount 在创建线程之前立即增加,而不是在线程开始运行时增加.

【讨论】:

  • 正确使用是什么意思?我知道引用计数在实际副本之前增加,我声称减量可以在该增加之前出现。我稍后会用一个例子来编辑我的帖子,以更好地说明我的意思。
  • @NinaKaprez 你没抓住重点。传递给复制构造函数的_Other 参数有自己对共享对象的引用,因此它的引用计数在进入构造函数时已经至少为1,然后将由构造函数递增。其他shared_ptrs 是否引用同一个共享对象无关紧要,在构造函数运行时(in|de)递增其引用计数。 refcount 将保持至少 1,即使所有其他引用都被清除,构造函数仍然会在 _Other 参数中具有活动引用,直到构造函数退出。
  • @NinaKaprez 在所有引用被清除并且它的引用计数降至 0 之前,共享对象不会被删除。并且通过 正确,我的意思是不要做一些愚蠢的事情,比如访问直接指向共享对象的原始指针并手动删除它,或者通过将其分配给多个拥有的智能指针等方式对其进行错误处理。
  • 我想我明白你的意思了。然而,我正在谈论一个非拥有线程调用 shared_ptr 复制构造函数的极端情况。我编辑了我的问题以包含一个示例。
  • @RemyLebeau:看看他的例子; lambda 通过引用 捕获堆栈对象;它不会复制该对象,直到它在另一个线程中执行。这两个线程竞相操作相同的shared_ptr 对象,而不是引用相同共享状态的副本。
【解决方案2】:

shared_ptr 对象共享状态的操作是线程安全的; shared_ptr 本身 不是线程安全的。您不能同时从不同的线程操作相同的shared_ptr 对象;尝试这样做是一场数据竞赛,因此是 UB。

所以如果lambda 在发送到另一个线程之前复制了指针,你的代码就可以了。

还应该注意的是,无论shared_ptr 是如何编写的,您的具体示例永远不会起作用。类型可以是atomic&lt;int&gt;,它仍然会被破坏。在 lambda 开始执行复制操作之前,您为 lambda 提供了对可能不存在的对象的引用。

再多的内部线程安全也无法将您救到那里。将堆栈变量的引用传递给另一个线程应始终视为代码异味。

【讨论】:

    猜你喜欢
    • 2015-03-03
    • 2013-01-07
    • 2019-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多