fetch_sub 和 memory_order_relaxed 用于原子引用计数?
Posted
技术标签:
【中文标题】fetch_sub 和 memory_order_relaxed 用于原子引用计数?【英文标题】:fetch_sub with memory_order_relaxed for atomic reference counting? 【发布时间】:2016-07-19 02:59:01 【问题描述】:std::atomic<int> cnt = 2;
thread 1:
doFoo();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1)
doBazz();
thread 2:
doBar();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1)
doBazz();
我们能保证doFoo()
和doBar()
总是在doBazz()
之前发生吗?
【问题讨论】:
您正在使用 memory_order_relaxed,这意味着没有执行内存排序。没有这样的保证。 【参考方案1】:如果线程 2 调用 doBazz()
,则可以保证 doBazz()
看到 doBar()
的所有副作用,因为它们在同一个线程上运行并且有 sequenced-before 它们之间的关系。但不能保证它会看到 doFoo()
的副作用,或者根本不会调用 doFoo()
。这是因为doFoo()
和doBazz()
之间没有形成happens-before 关系。如果 thread 1 在访问 cnt
时使用 std::memory_order_release
减少 cnt
并且 thread 2 使用 std::memory_order_acquire
,则将形成这种关系,这将在两者之间创建一个同步点他们。
线程1调用doBazz()
的情况是对称的。
因此,在两个线程中使用cnt.fetch_sub(1, std::memory_order_acq_rel)
(std::memory_order_acq_rel
结合了std::memory_order_acquire
和std::memory_order_release
)将提供您所询问的保证。
我们也可以使用cnt.fetch_sub(1, std::memory_order_release)
并在调用doBazz()
之前调用std::atomic_thread_fence(std::memory_order_acquire)
来达到同样的效果。
【讨论】:
【参考方案2】:您显示的代码中根本没有内存排序,因此保证不成立。但是,通过将fetch_sub
设为release sequence 的一部分,可以在您所在的位置使用轻松的排序并使其正常工作:
std::atomic<int> cnt0;
cnt.store(2, std::memory_order_release); // initiate release sequence (RS)
//thread 1:
doFoo();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) // continue RS
std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
doBazz();
//thread 2:
doBar();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) // continue RS
std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
doBazz();
或
void doBazz();
std::atomic<int> cnt0;
cnt.store(2, std::memory_order_release); // initiate release sequence (RS)
//thread 1:
doFoo();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) // continue RS
doBazz();
//thread 2:
doBar();
if (cnt.fetch_sub(1, std::memory_order_relaxed) == 1) // continue RS
doBazz();
void doBazz()
std::atomic_thread_fence(std::memory_order_acquire); // synchronizes with RS
// ...
这些保证doFoo()
和doBar()
总是在doBazz()
之前发生。
【讨论】:
Acquire 不会阻止代码在操作下方移动。 我不明白你是如何防止这种情况发生的。 @2501 : 在 C++ 内存模型中,每个非松弛原子操作都充当编译器屏障。 这是不正确的。 Acquire 仅充当阻止在其上方移动代码的屏障。反之亦然。你需要一个完整的屏障来防止代码双向移动。 是的,引用是正确的,但并非每个原子操作都充当完整屏障。这是您没有看到的相关差异。获取操作具有我之前引用的特定语义。是障碍吗?是的。它是一个完整的障碍吗?不,请阅读:preshing.com/20120913/acquire-and-release-semantics/… 并观看:channel9.msdn.com/Shows/Going+Deep/…【参考方案3】:http://en.cppreference.com/w/cpp/atomic/memory_order
即使使用宽松的内存模型,也不允许凭空产生的值循环依赖于它们自己的计算,例如,x 和 y 最初为零,
// Thread 1:
r1 = x.load(memory_order_relaxed);
if (r1 == 42) y.store(r1, memory_order_relaxed);
// Thread 2:
r2 = y.load(memory_order_relaxed);
if (r2 == 42) x.store(42, memory_order_relaxed);
不允许产生 r1 == r2 == 42,因为仅当存储到 x 存储 42 时才可能将 42 存储到 y,这循环依赖于存储到 y 存储 42。请注意,直到 C++ 14,这在技术上是规范允许的,但不推荐给实现者。
即使使用 memory_order_relaxed,仍然存在一些不允许的执行顺序。在我看来,
cnt.fetch_sub(1, std::memory_order_relaxed) == 2
应该发生在
cnt.fetch_sub(1, std::memory_order_relaxed) == 1
对吗?因此 doFoo() 和 doBar() 都应该在 doBazz() 之前发生。
【讨论】:
以上是关于fetch_sub 和 memory_order_relaxed 用于原子引用计数?的主要内容,如果未能解决你的问题,请参考以下文章
Linux C/C++并发编程实战C/C++ 6种内存时序memory_order详解
C++ 原子减 atomic::fetch_sub fetch_add
在 C++ 标准中滥用 std::memory_order::relaxed 的示例 [algorithms.parallel.exec/5 in n4713]
Linux C/C++并发编程实战2万字解读C/C++ 6种内存时序memory_order(内存屏障)指令重排原子操作