原子参考计数

时间:2015-07-06 20:13:00

标签: c++ multithreading c++11 atomic reference-counting

我试图准确理解线程安全的原子引用计数是如何工作的,例如与std::shared_ptr一样。我的意思是,基本概念很简单,但我真的很担心减法加上delete如何避免竞争条件。

这个tutorial from 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;
    }
  }
};

好的,所以我得到了一般的想法。但我不明白为什么不可能出现以下情况:

说refcount目前是1

  1. 线程A :原子地将引用计数减少到0
  2. 主题B :以原子方式将引用计数递增为1
  3. 线程A :在托管对象指针上调用delete
  4. 主题B :将引用计数视为1,访问托管对象指针... SEGFAULT!
  5. 我无法理解是什么阻止了这种情况发生,因为没有什么能阻止之间的数据竞争,当refcount达到0时,对象被删除。减少引用计数并调用delete是两个独立的非原子操作。那么没有锁定怎么可能呢?

6 个答案:

答案 0 :(得分:18)

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

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

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

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

如果你想要,例如以线程安全方式复制std::shared_ptr,您必须确保所有加载和存储都通过专用于shared_ptr的{​​{3}}操作发生。

答案 1 :(得分:5)

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

答案 2 :(得分:3)

您的方案无法实现,因为线程B应该已经使用递增的引用计数创建。线程B不应该首先递增引用计数。

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

如果线程A在不增加引用计数的情况下创建线程B,则可能会出现如您所述的不良事件。

答案 3 :(得分:3)

实施不提供或要求这样的保证,避免您所描述的行为是基于对计数参考的正确管理,通常通过诸如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,我们保留了当前的引用计数。如果我们通过非const ref,那么x()释放引用是可行的,而y将被调用null。

如果x()存储/保留原始指针,那么我们可能会遇到问题。当我们的函数返回时,refcount可能达到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

VS

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
}

答案 4 :(得分:1)

  

线程B:以原子方式将refcount递增为1。

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

线程B都有对象的引用,或者它没有引用。如果是,则引用计数不能为零。如果没有,那么当它没有引用该对象时,为什么它会搞乱智能指针管理的对象呢?

答案 5 :(得分:1)

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

关于boost::intrusive_ptr<X>,这不是答案。