考虑以下简单的引用计数函数(与boost::intrusive_ptr
一起使用):
class Foo {
// ...
std::atomic<std::size_t> refCount_{0};
friend void intrusive_ptr_add_ref(Foo* ptr)
{
++ptr->refCount_; // ❶
}
friend void intrusive_ptr_release(Foo* ptr)
{
if (--ptr->refCount_ == 0) { // ❷
delete ptr;
}
}
};
我还在学习内存排序,我想知道fetch_add/sub
(memory_order_seq_cst
)的默认内存排序是否过于严格。由于我想要确保的唯一订单是在❶和between之间,我认为我们可以用
ptr->refCount_.fetch_add(1, std::memory_order_release);
和❷with
if (ptr->refCount_.fetch_sub(1, std::memory_order_consume) == 1) {
但是内存排序对我来说仍然是新的和微妙的,所以我不确定这是否会正常工作。我错过了什么吗?
答案 0 :(得分:2)
咨询std::shared_ptr
的libc ++实现,您可能希望memory_order_relaxed
用于递增,memory_order_acq_rel
用于递减。合理化这种用法:
如果数量增加,那么重要的是它的一致性。当前线程已经确定它大于零。其他线程是不同步的,因此他们将在下一次原子修改之前的不确定时间看到更新,并且可能在与其他变量更新不一致的时候。
如果数字减少,那么您需要确保当前线程已经完成修改。必须显示其他线程的所有更新。当前的减量必须对下一个减少。否则,如果计数器在它守卫的物体前面比赛,则物体可能会过早被摧毁。
Cppreference有一个很好的内存排序页面。它包括这个注释:
正在修订发布 - 消费排序规范,暂时不建议使用
memory_order_consume
。
它还暗示没有当前的编译器或CPU实现consume
;它实际上与acquire
相同。
答案 1 :(得分:1)
增加引用计数不需要在正确的程序中进行任何同步,只需要原子性。
我们假装引用归线程所有。如果引用计数器至少为一个,则线程只能使用引用的对象,并且在使用对象时保证不会降为零,这意味着线程在使用对象期间增加了引用计数器,或者有另一种机制可以确保满足这一条件。
因此,我们假设递增引用计数的线程拥有确保它可以访问对象的引用计数器的引用,因此在尝试递增计数器时,没有其他线程可以将引用计数器减少为零。允许删除初始引用的唯一线程是当前线程(在递增引用计数之后),或者当前线程已经发信号通知其对象的共享使用(即原始引用的“所有权”)具有的另一个线程停止 - 这些都是可见效果。
另一方面,递减引用计数器需要获取和释放语义,因为之后对象可能会被破坏。
std::memory_order
上的CPP参考页面
放宽内存排序的典型用法是递增计数器,例如std :: shared_ptr的引用计数器,因为这只需要原子性,但不需要排序或同步(请注意,递减shared_ptr计数器需要与析构函数进行获取 - 释放同步)。
答案 2 :(得分:0)
if (ptr->refCount_.fetch_sub(1, std::memory_order_consume) == 1) {
那显然是错误的:使用std::memory_order_consume
才有意义,但后来却产生了可以消耗的值,就像这样:
r = a.fetch_sub(1, std::memory_order_consume);
v[r] = 1; // depends on r
x = r+2; // depends on r
z = r-r; // this is a legal dependency but many compilers mis-compile it
y = r/r; // still a legal dependency
f (r); // where f is declared with carry dependency
r2 = r; // transfers the dependency
v[r2] = 2; // dependency ordered
r2 = r ? 1 : 0; // breaks the dependency
v[r2] = 2; // not dependency ordered
在大多数CPU上,消耗都是零成本订购,因为CPU仅仅依靠与值有关的结果即可保证正确的订购。假定汇编代码精确地镜像了C ++代码,而编译器通常无法保证。考虑这些:
int a1[1];
a[r] = 2; // r is 0 or out of bound
在这里,编译器可以假定r为0:正好有一个元素,因此代码的作用与以下内容相同:
a[0] = 2;
但是那个人不依赖r
,因此在消费订购的情况下,汇编代码必须使用r
进行寻址。
这意味着编译器必须能够有选择地禁用许多常见,简单的优化,这些优化是在编译器的另一部分(与语言无关的一部分)中完成的。
这些显而易见的情况很多都必须加以优化,必须精心设计一些情况,例如指向单例的指针。