C++ std::shared_ptr 的递增和递减引用计数的竞赛 [duplicate]

C++ Race of incrementing and decrementing reference counting of std::shared_ptr [duplicate]

提问人:Bin Yan 提问时间:11/9/2022 最后编辑:EvgBin Yan 更新时间:11/9/2022 访问量:268

问:

从引用中,我知道它本身是线程安全的,因为引用计数通常由一些具有 .std::shared_ptr<T>std::atomicstd::memory_order_relaxed

但是,我仍然不知道如何在引用计数器的并发递增和递减下确保线程安全。std::shared_ptr

即,

Thread 1:
// destructing shared_ptr<T>
// rc.fetch_sub(1)
// Thread 1 got RC == 0 and *thought* that he is the last owner
// ** call T::~T()

Thread 2:
// concurrently copy that shared_ptr<T>
// rc.fetch_add(1)
// Thread 2 got RC == 1 and *thought* that the copied shared_ptr is valid
// dereference the shared_ptr
// ** accessing the destructed T !

这种情况虽然是种族,但并非不可能。 下面是一个示例代码(手动构造一个极端情况)。

std::shared_ptr<T> ptr;
int main()
{
    std::thread t([&ptr] () 
    {
        ptr = std::make_shared<int>();
    } // reference counting decrease here! Call ~T()
    );

    auto ptr2 = ptr; // reference counting increase here! 
    ptr2->some_function(); // access destructed object!

    t.join();
}

我的问题是:

  • C++ 参考如何说明这种情况?
  • 即使在引用计数器的并发递增和递减下,是否也能确保线程安全?std::shared_ptr<T>
C++ 多线程 std shared-ptr

评论

1赞 François Andrieux 11/9/2022
您的示例包含 上的争用条件。该标准不保证它是原子的,只保证参考计数器是原子的。ptrstd::shared_ptr
2赞 François Andrieux 11/9/2022
请分享一个最小的可重复示例。显示的代码无法编译。什么?您被分配了 .Tshared_ptr<T>shared_ptr<int>
5赞 François Andrieux 11/9/2022
您描述的情况既是争用条件,也是对象生存期管理错误。您描述的是 UB,即使类型是原子的。您不能销毁一个对象,也不能从另一个对象访问它。编辑:您可能会将销毁 a 与销毁指向的对象混淆,这是不同的关注点。shared_ptr
1赞 ShadowRanger 11/9/2022
呃。引用计数如何减少? 在 lambda 中通过引用接收并分配;引用计数变为(由于创建了它)并且永远不会减少(好吧,直到程序结束时全局变量被销毁)。reference counting decrease here! Call ~T()ptr1make_shared
1赞 Richard Critten 11/9/2022
不要在超过 1 个线程中使用相同的内容。访问不是线程安全的。您应该始终在每个线程中使用 a 的唯一副本。问题从这里开始,使用对它的引用,应该改为使用 .std::shared_ptrptrstd::shared_ptrstd::thread t([&ptr] ()...ptrptr

答:

2赞 Wintermute 11/9/2022 #1

shared_ptr在内部不使用 etc.,而是使用 .从 GNU 实现:fetch_addcompare_exchange

  template<>
    inline bool
    _Sp_counted_base<_S_atomic>::
    _M_add_ref_lock_nothrow() noexcept
    {
      // Perform lock-free add-if-not-zero operation.
      _Atomic_word __count = _M_get_use_count();
      do
        {
          if (__count == 0)
            return false;
          // Replace the current counter value with the old value + 1, as
          // long as it's not changed meanwhile.
        }
      while (!__atomic_compare_exchange_n(&_M_use_count, &__count, __count + 1,
                                          true, __ATOMIC_ACQ_REL,
                                          __ATOMIC_RELAXED));
      return true;
    }

有一个小样板,但本质上它的作用是获取当前计数,检查它是否为零,否则在期望值自读取以来没有更改的情况下进行原子compare_exchange运算。这是无锁(但不是无等待)数据结构中相当常见的机制。如果你稍微眯着眼睛,你可以称它为一种用户空间自旋锁。与互斥锁相比,这是有利的,因为它可以节省非常昂贵的系统调用,除非shared_ptr上的争用非常高。

顺便说一句,您的代码应该在使用前检查是否有效,因为线程可能在主线程中的复制之前就已经结束了。不过,这似乎与您的实际问题无关。ptr2

评论

0赞 François Andrieux 11/9/2022
shared_ptr的分配不是原子的。原始代码中仍存在争用条件。检查是否有效是不够的。如果尚未进入UB中,则不可能出现问题中描述的场景,并表明存在潜在的混淆。ptrptr2
0赞 Wintermute 11/9/2022
是否确定?我以为有保证,只是没有它引用的对象。shared_ptr
3赞 François Andrieux 11/9/2022
只有控制块是同步的。这意味着,如果您有两个线程,每个线程都对同一对象有自己的线程并使用相同的引用计数器,则每个线程都可以安全地重新分配或重置自己的线程。但是,如果两个线程对同一指针执行相同的操作,则不允许它们避免数据争用。shared_ptrshared_ptrshared_ptr
1赞 François Andrieux 11/9/2022
请参阅 en.cppreference.com/w/cpp/memory/shared_ptr:所有成员函数(包括复制构造函数和复制赋值)都可以由不同shared_ptr实例上的多个线程调用,而无需额外的同步,即使这些实例是副本并共享同一对象的所有权。如果多个执行线程在没有同步的情况下访问同一个shared_ptr实例,并且其中任何一个访问使用非常量成员函数 shared_ptr,则将发生数据争用。
5赞 François Andrieux 11/9/2022
问题很简单,在一个线程和另一个线程中,没有同步并使用相同的对象是 UB。 没有提供例外。ptr = /*something*/;/*something/* = ptr;ptrshared_ptr