在具有相同顺序的原子 load/store 之前使用 std::atomic_thread_fence 总是多余的吗?
Is using std::atomic_thread_fence right before an atomic load/store with the same order always redundant?
鉴于:
std::atomic<uint64_t> b;
void f()
{
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
uint64_t a = b.load(std::memory_order::memory_order_acquire);
// code using a...
}
删除对 std::atomic_thread_fence
的调用有什么影响吗?如果是这样,有一个简洁的例子吗?请记住,其他函数可能 store/load 到 b
并调用 f
.
从不多余。 atomic_thread_fence
实际上比 mo_acquire
的负载有更严格的排序要求。它的记录很少,但是获取栅栏不是单向加载的。它保留了栅栏两侧访问之间的读写顺序。
另一方面,加载获取只需要在该加载与后续加载和存储之间进行排序。 Read-Read 和 Read-Write 顺序仅在该特定加载-获取之间强制执行。 Prior loads/stores(按程序顺序)没有限制。因此,负载获取是单向的。
释放栅栏同样失去了存储的单向渗透性,保留了写入-读取和写入-写入。请参阅 Jeff Preshing 的文章 https://preshing.com/20130922/acquire-and-release-fences/。
顺便说一下,你的栅栏好像弄错了。请参阅 Preshing 的另一篇文章 https://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/。对于获取加载,加载发生在获取之前,因此使用栅栏它看起来像这样:
uint64_t a = b.load(std::memory_order::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
请记住,发布并不能保证可见性。所有释放所做的都是保证写入不同变量的顺序在其他线程中变得可见。 (没有这个,其他线程可以观察到似乎违反因果关系的顺序。)
这是一个使用 CppMem 工具的示例 (http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/)。第一个线程是 SC,所以我们知道写入是按这个顺序发生的。所以如果 c==1,那么 a 和 b 也应该都是 1。 CppMem 给出“48 次执行;1 次一致,无竞争”,表明第二个线程有可能看到 c==1 && b==0 && a==0。这是因为c.load
允许在a.load
之后重新排序,渗透过去b.load
int main() {
atomic_int a = 0;
atomic_int b = 0;
atomic_int c = 0;
{{{ {
a.store(1, mo_seq_cst);
b.store(1, mo_seq_cst);
c.store(1, mo_seq_cst);
} ||| {
c.load(mo_relaxed).readsvalue(1);
b.load(mo_acquire).readsvalue(0);
a.load(mo_relaxed).readsvalue(0);
} }}}
}
如果我们用 aquire-fence 替换 acquire-load,则 c.load
不允许在 a.load
之后重新排序。 CppMem 给出“8 次执行;不一致”确认这是不可能的。
int main() {
atomic_int a = 0;
atomic_int c = 0;
{{{ {
a.store(1, mo_seq_cst);
c.store(1, mo_seq_cst);
} ||| {
c.load(mo_relaxed).readsvalue(1);
atomic_thread_fence(mo_acquire);
a.load(mo_relaxed).readsvalue(0);
} }}}
}
编辑: 改进了第一个示例以实际显示跨获取操作的变量。
鉴于:
std::atomic<uint64_t> b;
void f()
{
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
uint64_t a = b.load(std::memory_order::memory_order_acquire);
// code using a...
}
删除对 std::atomic_thread_fence
的调用有什么影响吗?如果是这样,有一个简洁的例子吗?请记住,其他函数可能 store/load 到 b
并调用 f
.
从不多余。 atomic_thread_fence
实际上比 mo_acquire
的负载有更严格的排序要求。它的记录很少,但是获取栅栏不是单向加载的。它保留了栅栏两侧访问之间的读写顺序。
另一方面,加载获取只需要在该加载与后续加载和存储之间进行排序。 Read-Read 和 Read-Write 顺序仅在该特定加载-获取之间强制执行。 Prior loads/stores(按程序顺序)没有限制。因此,负载获取是单向的。
释放栅栏同样失去了存储的单向渗透性,保留了写入-读取和写入-写入。请参阅 Jeff Preshing 的文章 https://preshing.com/20130922/acquire-and-release-fences/。
顺便说一下,你的栅栏好像弄错了。请参阅 Preshing 的另一篇文章 https://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/。对于获取加载,加载发生在获取之前,因此使用栅栏它看起来像这样:
uint64_t a = b.load(std::memory_order::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
请记住,发布并不能保证可见性。所有释放所做的都是保证写入不同变量的顺序在其他线程中变得可见。 (没有这个,其他线程可以观察到似乎违反因果关系的顺序。)
这是一个使用 CppMem 工具的示例 (http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/)。第一个线程是 SC,所以我们知道写入是按这个顺序发生的。所以如果 c==1,那么 a 和 b 也应该都是 1。 CppMem 给出“48 次执行;1 次一致,无竞争”,表明第二个线程有可能看到 c==1 && b==0 && a==0。这是因为c.load
允许在a.load
之后重新排序,渗透过去b.load
int main() {
atomic_int a = 0;
atomic_int b = 0;
atomic_int c = 0;
{{{ {
a.store(1, mo_seq_cst);
b.store(1, mo_seq_cst);
c.store(1, mo_seq_cst);
} ||| {
c.load(mo_relaxed).readsvalue(1);
b.load(mo_acquire).readsvalue(0);
a.load(mo_relaxed).readsvalue(0);
} }}}
}
如果我们用 aquire-fence 替换 acquire-load,则 c.load
不允许在 a.load
之后重新排序。 CppMem 给出“8 次执行;不一致”确认这是不可能的。
int main() {
atomic_int a = 0;
atomic_int c = 0;
{{{ {
a.store(1, mo_seq_cst);
c.store(1, mo_seq_cst);
} ||| {
c.load(mo_relaxed).readsvalue(1);
atomic_thread_fence(mo_acquire);
a.load(mo_relaxed).readsvalue(0);
} }}}
}
编辑: 改进了第一个示例以实际显示跨获取操作的变量。