gpt4 book ai didi

c++ - 如何在 C++11 中实现 StoreLoad 屏障?

转载 作者:行者123 更新时间:2023-12-01 09:26:04 26 4
gpt4 key购买 nike

我想编写可移植代码(Intel、ARM、PowerPC...)来解决一个经典问题的变体:

Initially: X=Y=0

Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }

其中 目标是避免出现两个线程都在做 something 的情况。 . (如果两个都没有运行也没关系;这不是只运行一次的机制。)
如果您在下面的推理中发现一些缺陷,请纠正我。

我知道,我可以通过 memory_order_seq_cst 实现目标。原子 store s 和 load s 如下:
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}

这实现了目标,因为必须有一些单一的总订单 {x.store(1), y.store(1), y.load(), x.load()}事件,必须与程序顺序“边缘”一致:
  • x.store(1) “在TO之前”y.load()
  • y.store(1) “在TO之前”x.load()

  • 如果 foo()被调用,然后我们有额外的优势:
  • y.load() “读取之前的值”y.store(1)

  • 如果 bar()被调用,然后我们有额外的优势:
  • x.load() “读取之前的值”x.store(1)

  • 所有这些边组合在一起将形成一个循环:
    x.store(1) “在TO之前” y.load() “读取之前的值” y.store(1) “在TO之前” x.load() “读取之前的值” x.store(true)
    这违反了订单没有周期的事实。

    我故意使用非标准术语“在 TO 之前”和“读取值之前”,而不是像 happens-before 这样的标准术语。 ,因为我想征求关于我假设这些边确实暗示 happens-before 的正确性的反馈。关系,可以在单个图中组合在一起,并且这种组合图中的循环是禁止的。我不确定。我所知道的是这段代码在 Intel gcc & clang 和 ARM gcc 上产生了正确的障碍

    现在,我真正的问题有点复杂,因为我无法控制“X”——它隐藏在一些宏、模板等后面,可能比 seq_cst 弱。

    我什至不知道“X”是单个变量还是其他一些概念(例如轻量级信号量或互斥量)。我只知道我有两个宏 set()check()使得 check()返回 true “之后”另一个线程调用了 set() . (也知道 setcheck 是线程安全的,不能创建数据竞争 UB。)

    所以在概念上 set()有点像 "X=1"和 check()就像“X”,但我无法直接访问所涉及的原子(如果有的话)。
    void thread_a(){
    set();
    if(!y.load()) foo();
    }
    void thread_b(){
    y.store(1);
    if(!check()) bar();
    }

    我很担心,那个 set()可能在内部实现为 x.store(1,std::memory_order_release)和/或 check()可能是 x.load(std::memory_order_acquire) .或者假设是 std::mutex一个线程正在解锁,另一个线程是 try_lock ing;在 ISO 标准中 std::mutex只保证有获取和释放顺序,而不是 seq_cst。

    如果是这种情况,那么 check()的 if 正文可以在 y.store(true) 之前“重新排序” (请参阅 Alex's answer,他们证明这种情况发生在 PowerPC 上)。
    这真的很糟糕,因为现在这一系列事件是可能的:
  • thread_b()首先加载 x 的旧值( 0 )
  • thread_a()执行一切,包括 foo()
  • thread_b()执行一切,包括 bar()

  • 所以,两个 foo()bar()被叫了,我不得不避免。我有什么选择可以防止这种情况发生?

    选项 A

    尝试强制存储加载屏障。这在实践中可以通过 std::atomic_thread_fence(std::memory_order_seq_cst); 来实现。 - 正如 Alex in a different answer 所解释的所有经过测试的编译器都发出了完整的围栏:

    • x86_64: MFENCE
    • PowerPC: hwsync
    • Itanuim: mf
    • ARMv7 / ARMv8: dmb ish
    • MIPS64: sync


    这种方法的问题是,我在 C++ 规则中找不到任何保证,即 std::atomic_thread_fence(std::memory_order_seq_cst)必须转换为完整的内存屏障。其实 atomic_thread_fence的概念C++ 中的 s 似乎处于与内存屏障的汇编概念不同的抽象级别,并且更多地处理诸如“什么原子操作与什么同步”之类的东西。是否有任何理论证据表明以下实现实现了目标?
    void thread_a(){
    set();
    std::atomic_thread_fence(std::memory_order_seq_cst)
    if(!y.load()) foo();
    }
    void thread_b(){
    y.store(true);
    std::atomic_thread_fence(std::memory_order_seq_cst)
    if(!check()) bar();
    }

    选项 B

    使用我们对 Y 的控制来实现同步,通过对 Y 使用 read-modify-write memory_order_acq_rel 操作:
    void thread_a(){
    set();
    if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
    }
    void thread_b(){
    y.exchange(1,std::memory_order_acq_rel);
    if(!check()) bar();
    }

    这里的想法是对单个原子( y )的访问必须形成所有观察者都同意的单一顺序,因此 fetch_add之前 exchange反之亦然。

    fetch_add之前 exchange然后是 fetch_add 的“释放”部分与 exchange 的“获取”部分同步以及 set() 的所有副作用必须对执行的代码可见 check() , 所以 bar()不会被调用。

    否则, exchange之前 fetch_add ,然后是 fetch_add会看到 1而不是打电话 foo() .
    因此,不可能同时调用 foo()bar() .这个推理正确吗?

    选项 C

    使用虚拟原子,引入防止灾难的“边缘”。考虑以下方法:
    void thread_a(){
    std::atomic<int> dummy1{};
    set();
    dummy1.store(13);
    if(!y.load()) foo();
    }
    void thread_b(){
    std::atomic<int> dummy2{};
    y.store(1);
    dummy2.load();
    if(!check()) bar();
    }

    如果您认为这里的问题是 atomic s 是本地的,然后想象将它们移动到全局范围,在下面的推理中,这对我来说似乎无关紧要,我故意以这种方式编写代码来暴露 dummy1 和 dummy2 完全分开是多么有趣。

    为什么这可能有效?嗯,肯定有 {dummy1.store(13), y.load(), y.store(1), dummy2.load()} 的单个总订单这必须与程序顺序“边缘”一致:
  • dummy1.store(13) “在TO之前”y.load()
  • y.store(1) “在TO之前”dummy2.load()

  • (一个 seq_cst 存储 + 加载有望形成包括 StoreLoad 在内的完整内存屏障的 C++ 等价物,就像它们在真实 ISA 上的 asm 中所做的那样,甚至包括不需要单独屏障指令的 AArch64。)

    现在,我们有两种情况需要考虑: y.store(1)之前 y.load()或在总顺序之后。

    y.store(1)之前 y.load()然后 foo()不会被召唤,我们很安全。

    y.load()之前 y.store(1) ,然后将它与我们在程序顺序中已有的两条边结合起来,我们推断出:
  • dummy1.store(13) “在TO之前”dummy2.load()

  • 现在, dummy1.store(13)是释放操作,释放 set()的效果, 和 dummy2.load()是一个获取操作,所以 check()应该会看到 set() 的效果因此 bar()不会被召唤,我们很安全。

    在这里认为 check() 是否正确?会看到 set()的结果? 我可以像这样组合各种“边缘”(“程序顺序”又名“Sequenced Before”、“总顺序”、“发布前”、“获取后”)吗? 我对此表示严重怀疑:C++ 规则似乎在谈论同一位置上存储和加载之间的“同步”关系——这里没有这种情况。

    请注意,我们只担心 dumm1.store 的情况已知(通过其他推理)在 dummy2.load 之前在 seq_cst 总顺序中。因此,如果他们一直在访问同一个变量,负载就会看到存储的值并与之同步。

    (对于原子加载和存储编译为至少 1 路内存屏障(并且 seq_cst 操作不能重新排序:例如 seq_cst 存储不能通过 seq_cst 加载)的实现的内存屏障/重新排序推理是任何加载/存储在 dummy2.load 之后的其他线程肯定会在 y.store 之后可见。对于另一个线程类似,...之前 y.load 。)

    你可以在 https://godbolt.org/z/u3dTa8 玩我对选项 A、B、C 的实现

    最佳答案

    选项 A 和 B 是有效的解决方案。

  • 选项 A:seq-cst 栅栏翻译成什么并不重要,C++ 标准清楚地定义了它提供的保证。我在这篇文章中列出了它们:When is a memory_order_seq_cst fence useful?
  • 选项B:是的,你的推理是正确的。对某个对象的所有修改都有一个总顺序(修改顺序),因此您可以使用它来同步线程并确保所有副作用的可见性。

  • 但是,选项 C 无效!同步关系只能通过对同一对象的获取/释放操作来建立。在您的情况下,您有两个完全不同且独立的对象 dummy1dummy2 .但是这些不能用于建立一个发生在之前的关系。事实上,由于原子变量是纯粹的局部变量(即,它们只被一个线程触及),编译器可以根据 as-if 规则自由地删除它们。

    更新

    选项 A:
    我假设 set()check()对一些原子值进行操作。那么我们有以下情况(->表示sequential-before):
  • set() -> fence1(seq_cst) -> y.load()
  • y.store(true) -> fence2(seq_cst) -> check()

  • 所以我们可以应用以下规则:

    For atomic operations A and B on an atomic object M, where A modifies M and B takes its value, if there are memory_order_seq_cst fences X and Y such that A is sequenced before X, Y is sequenced before B, and X precedes Y in S, then B observes either the effects of A or a later modification of M in its modification order.



    即,要么 check()看到存储在 set 中的值, 或 y.load()看到写入的值是 y.store() (对 y 的操作甚至可以使用 memory_order_relaxed )。

    选项 C:
    C++17 standard状态 [32.4.3, p1347]:

    There shall be a single total order S on all memory_order_seq_cst operations, consistent with the "happens before" order and modification orders for all affected locations [...]



    这里的重要词是“一致”。这意味着如果操作 A 在操作 B 之前发生,那么在 S 中 A 必须在 B 之前。 然而,逻辑蕴涵是单向路,所以我们不能推断出逆:仅仅因为某些操作 C 在操作 D 之前在 S 中并不意味着 C 在 D 之前发生。

    特别是,两个单独对象上的两个 seq-cst 操作不能用于建立发生在关系之前,即使这些操作在 S 中是完全有序的。如果要对单独的对象进行排序操作,则必须引用 seq-cst-fences(参见选项 A)。

    关于c++ - 如何在 C++11 中实现 StoreLoad 屏障?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60053973/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com