繁体   English   中英

原子引用计数

[英]Atomic Reference Counting

我试图确切地了解线程安全的原子引用计数是如何工作的,例如与std::shared_ptr 我的意思是,基本概念很简单,但我真的很困惑 deref 加delete如何避免竞争条件。

来自 Boost 的教程演示了如何使用 Boost 原子库(或 C++11 原子库)实现原子线程安全引用计数系统。

#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>

class X {
public:
  typedef boost::intrusive_ptr<X> pointer;
  X() : refcount_(0) {}

private:
  mutable boost::atomic<int> refcount_;
  friend void intrusive_ptr_add_ref(const X * x)
  {
    x->refcount_.fetch_add(1, boost::memory_order_relaxed);
  }
  friend void intrusive_ptr_release(const X * x)
  {
    if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
      boost::atomic_thread_fence(boost::memory_order_acquire);
      delete x;
    }
  }
};

好的,所以我明白了一般的想法。 但我不明白为什么以下场景是不可能的:

假设引用计数当前为1

  1. 线程 A :以原子方式将 refcount 递减为0
  2. 线程 B :原子地将引用计数增加到1
  3. 线程 A :调用托管对象指针上的delete
  4. 线程 B :将引用计数视为1 ,访问托管对象指针... SEGFAULT!

我无法理解是什么阻止了这种情况的发生,因为在引用计数达到 0 和对象被删除之间没有什么可以阻止数据竞争。 减少引用计数和调用delete是两个独立的非原子操作。 那么没有锁怎么可能呢?

您可能高估了 shared_ptr 提供的线程安全性。

原子引用计数的本质是确保如果访问/修改shared_ptr两个不同实例(管理同一对象),则不会出现竞争条件。 但是,如果两个线程访问同一个shared_ptr对象(其中一个是写入),则shared_ptr并不能确保线程安全。 一个例子是,例如,如果一个线程取消引用指针,而另一个线程重置它。
所以关于shared_ptr唯一保证的是,只要在 shared_ptr 的单个实例上没有竞争(它也不会访问它指向线程安全的对象),就不会有双重删除和泄漏

因此,如果没有其他线程可以同时删除/重置它,那么创建 shared_ptr 的副本也是安全的(您也可以说,它不是内部同步的)。 这是你描述的场景。

再重复一遍:从多个线程访问单个shared_ptr实例,其中一个访问是对指针的写入仍然是竞争条件

如果你想以线程安全的方式复制一个std::shared_ptr ,你必须确保所有的加载和存储都通过std::atomic_...专门用于shared_ptr操作发生。

这种情况永远不会出现。 如果共享指针的引用计数达到 0,则对它的最后一个引用已被删除,删除指针是安全的。 由于没有可以复制的实例,因此无法创建对共享指针的另一个引用。

您的场景是不可能的,因为线程 B 应该已经创建并增加了引用计数。 线程 B 不应该像它做的第一件事那样增加引用计数。

假设线程 A 产生线程 B。线程 A 有责任在创建线程之前增加对象的引用计数,以保证线程安全。 线程 B 只需在退出时调用 release 即可。

如果线程 A 在不增加引用计数的情况下创建线程 B,则可能会发生您所描述的不好的事情。

该实现不提供或不需要这样的保证,避免您所描述的行为取决于对计数引用的正确管理,通常通过诸如std::shared_ptr类的 RAII 类来完成。 关键是要完全避免跨作用域传递原始指针。 任何存储或保留指向对象的指针的函数都必须采用共享指针,以便它可以正确地增加引用计数。

void f(shared_ptr p) {
   x(p); // pass as a shared ptr
   y(p.get()); // pass raw pointer
}

这个函数被传递了一个shared_ptr所以引用计数已经是 1+。 我们的本地实例p应该在复制赋值期间增加了 ref_count。 当我们通过值传递时调用x时,我们创建了另一个引用。 如果我们通过 const ref 传递,我们会保留当前的 ​​ref 计数。 如果我们通过非常量引用传递,那么x()释放引用是可行的,并且y将使用 null 调用。

如果x()存储/保留原始指针,那么我们可能会遇到问题。 当我们的函数返回时,引用计数可能会达到 0,并且对象可能会被销毁。 这是我们没有正确维护引用计数的错。

考虑:

template<typename T>
void test()
{
    shared_ptr<T> p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:2
    } // ~q -> rc:1
    use(p.get()); // valid
} // ~p -> rc:0 -> delete

对比

template<typename T>
void test()
{
    T* p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:1
    } // ~q -> rc:0 -> delete
    use(p); // bad: accessing deleted object
}

线程 B:原子地将引用计数增加到 1。

不可能的。 要将引用计数增加到 1,引用计数必须为零。 但是如果引用计数为零,线程 B 是如何访问该对象的呢?

线程 B 要么有对象的引用,要么没有。 如果是,则引用计数不能为零。 如果没有,那么当它没有引用该对象时,为什么它会与智能指针管理的对象混淆?

对于std::shared_ptr ,引用计数更改是线程安全的,但不是对 `shared_ptr 内容的访问。

关于boost::intrusive_ptr<X> ,这是没有答案的。

暂无
暂无

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

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