【问题标题】:Is using std::atomic_thread_fence right before an atomic load/store with the same order always redundant?在具有相同顺序的原子加载/存储之前使用 std::atomic_thread_fence 总是多余的吗?
【发布时间】:2021-12-12 01:24:21
【问题描述】:

给定:

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 的调用是否有任何效果?如果是这样,有一个简洁的例子吗?请记住,其他函数可能会存储/加载到b 并调用f

【问题讨论】:

  • 使用std::atomic_thread_fence 相对于b 是多余的,因为您使用的是std::memory_order_acquire,因为该内存顺序要求在您进行读取之前任何写入都是可见的。但是,如果您要保护可能陈旧的非原子数据,则围栏很有用。但是这个代码还有其他问题。
  • 获取栅栏必须在原子操作之后进行排序才能产生效果。因此,在您的示例中,栅栏可能是多余的,但不是因为它在具有获取排序的原子操作之前。如果这不能回答您的问题,请编辑您的问题以更具体。
  • @Mgetz 你能解释一下你所说的其他问题是什么意思吗?

标签: c++ multithreading atomic lockless instruction-reordering


【解决方案1】:

从不冗余。 atomic_thread_fence 实际上比使用 mo_acquire 的负载有更严格的订购要求。它的文档记录很差,但是获取围栏不是单向允许负载的;它保留了栅栏相对两侧的访问之间的读-读和读-写顺序。

另一方面,加载获取只需要在加载和后续加载和存储之间进行排序。只读和读写顺序仅在特定的加载获取之间强制执行。先前的加载/存储(按程序顺序)没有限制。因此负载获取是单向的。

release fence 同样失去了 store 的单向性,保留了 Write-Read 和 Write-Write。请参阅 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);
  } }}}
}

编辑:改进了第一个示例,以实际显示跨获取操作的变量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-13
    • 2017-11-03
    • 1970-01-01
    • 1970-01-01
    • 2022-08-11
    • 1970-01-01
    相关资源
    最近更新 更多