我试图确切地了解线程安全的原子引用计数的工作原理,例如与std::shared_ptr
一样。 我的意思是,基本概念很简单,但我真的很困惑 decref 加 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
.
- 线程 A:以原子方式将引用计数转换为
0
。 - 线程 B:原子地将引用计数增加到
1
。 - 线程 A:调用托管对象指针上的
delete
。 - 线程 B:将 refcount 视为
1
,访问托管对象指针...塞格格特!
我不明白是什么阻止了这种情况的发生,因为在 refcount 达到 0 和对象被删除之间没有什么可以阻止数据竞争。 取消引用计数和调用delete
是两个独立的非原子操作。 那么没有锁怎么可能呢?
您可能高估了shared_ptr提供的线程安全性。
原子引用计数的本质是确保如果访问/修改shared_ptr
的两个不同实例(管理同一对象(,则不会有争用条件。但是,如果两个线程访问同一个shared_ptr
对象(其中一个是写入(,则shared_ptr
不能确保线程安全。例如,如果一个线程取消引用指针,而另一个线程重置它。
因此,shared_ptr
唯一保证的是,只要shared_ptr的单个实例上没有竞争,就不会有双重删除和泄漏(它也不会访问它指向线程安全的对象(
因此,如果没有其他线程可以同时删除/重置它(您也可以说它不是内部同步的(,创建shared_ptr的副本也是安全的。这是您描述的方案。
再次重复:从多个线程访问单个shared_ptr
实例,其中其中一个访问是写入指针仍然是一种争用条件。
例如,如果要以线程安全的方式复制std::shared_ptr
,则必须确保所有加载和存储都通过专门用于shared_ptr
的std::atomic_...
操作进行。
您的方案是不可能的,因为线程 B 应该已经使用递增的引用计数创建。线程 B 不应该将引用计数递增为它做的第一件事。
假设线程 A 生成线程 B.线程 A 负责在创建线程之前增加对象的引用计数,以保证线程安全。然后,线程 B 只需在退出时调用 release。
如果线程 A 在不增加引用计数的情况下创建线程 B,则可能会发生您描述的坏事。
该实现不提供或不需要这样的保证,避免您所描述的行为是基于对计数引用的正确管理,通常通过 RAII 类(如 std::shared_ptr
(完成。关键是要完全避免通过原始指针跨范围传递。任何存储或保留指向对象的指针的函数都必须采用共享指针,以便它可以正确增加引用计数。
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
如果我们按值传递时,我们创建了另一个 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
与
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>
,这不是答案。