内存栅栏:acquire/load 和 release/store
Memory fences: acquire/load and release/store
我对std::memory_order_acquire
和std::memory_order_release
的理解如下:
Acquire 表示在之后 acquire fence 出现的内存访问不能被重新排序到fence 之前。
Release表示在释放栅栏之前出现的内存访问不能被重新排序到栅栏之后。
我不明白的是,为什么特别是对于 C++11 原子库,获取栅栏与加载操作相关联,而释放栅栏与存储操作相关联。
澄清一下,C++11 <atomic>
库允许您以两种方式指定内存栅栏:要么您可以将栅栏指定为原子操作的额外参数,例如:
x.load(std::memory_order_acquire);
或者你可以使用std::memory_order_relaxed
并单独指定围栏,如:
x.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
我不明白的是,鉴于上述获取和释放的定义,为什么C++11特意将acquire与load[=关联起来53=],以及 release with store?是的,我看过许多示例,这些示例展示了如何使用 acquire/load 和 release/store 在线程之间进行同步,但总的来说,获取栅栏的想法(防止内存重新排序之后语句)和释放栅栏(防止语句之前的内存重新排序)与加载和存储的想法正交。
那么,为什么编译器不让我说:
x.store(10, std::memory_order_acquire);
我意识到我可以通过使用 memory_order_relaxed
,然后使用单独的 atomic_thread_fence(memory_order_acquire)
语句来完成上述操作,但是同样,为什么我不能直接使用 memory_order_acquire
存储?
一个可能的用例可能是,如果我想确保某些存储,比如 x = 10
,发生在 之前 一些可能影响其他线程的其他语句执行.
假设我写了一些数据,然后我写了一个指示,表明数据现在已经准备好了。看到数据已准备好指示的任何其他线程都必须看到数据本身的写入。因此,先前的写入不能超过该写入。
假设我读到一些数据已准备就绪。看到数据准备就绪后,我发出的任何读取都必须发生。因此后续读取不能移动到该读取之后。
因此,当您执行同步写入时,您通常需要确保您之前执行的所有写入都对看到同步写入的任何人可见。当您执行同步读取时,您之后执行的任何读取通常都必须在同步读取之后进行。
或者,换句话说,一个acquire通常是读取你可以获取或访问资源,并且后续的读取和写入一定不能在它之前移动。一个版本通常是写你已经完成了资源,并且前面的写不能移动到它之后。
std::memory_order_acquire
fence 仅确保在 fence 之后的所有 load 操作不会在 fence 之前的任何 load 操作之前重新排序,因此 memory_order_acquire
不能 确保存储在执行加载后对其他线程可见。这就是为什么 memory_order_acquire
不支持 store 操作的原因,你可能需要 memory_order_seq_cst
来实现 store 的获取。
或者,您可以说
x.store(10, std::memory_order_releaxed);
x.load(std::memory_order_acquire); // this introduce a data dependency
确保所有货品在入店前不会重新订购。同样,围栏在这里不起作用。
此外,原子操作中的内存顺序可能比内存栅栏更便宜,因为它只确保相对于原子指令的顺序,而不是栅栏前后的所有指令。
有关详细信息,另请参阅 formal description and explanation。
(纠正问题早期部分错误的部分答案。 already nicely covers the main question you're asking. Jeff Preshing's article on acquire / release 也是很好的阅读材料。)
你给的acquire/release的定义对于fences是错误的;它们仅适用于获取 operations 和释放 operations,如 x.store(mo_release)
,而不是 std::atomic_thread_fence(mo_release)
.
Acquire means that no memory accesses which appear after the acquire fence can be reordered to before the fence. [wrong, would be correct for acquire operation]
Release means that no memory accesses which appear before the release fence can be reordered to after the fence. [wrong, would be correct for release operation]
它们不足以用于栅栏,这就是为什么 ISO C++ 对获取栅栏(阻止 LoadStore / LoadLoad 重新排序)和释放栅栏(LoadStore / StoreStore)具有更强的排序规则。
当然,ISO C++ 没有定义“重新排序”,这意味着您正在访问一些全局一致的状态。改为 ISO C++
Jeff Preshing 的文章与此处相关:
- Acquire and Release Semantics(获取/释放 操作,例如加载、存储和 RMW)
- Acquire and Release Fences Don't Work the Way You'd Expect 解释了为什么那些单向屏障定义不正确且不足以用于围栏,这与操作不同。 (因为它会让围栏一直重新排序到程序的一端,并使所有操作彼此无序,因为它与操作本身无关。)
A possible use case for this might be if I want to ensure that some store, say x = 10, happens before some other statement executes that might affect other threads.
如果“其他语句”是来自原子共享变量的加载,您实际上需要 std::memory_order_seq_cst
来避免 StoreLoad 重新排序。 acquire
/ release
/ acq_rel
不会阻止它。
如果你的意思是确保原子存储在其他原子存储之前可见,正常的方法是让 2nd 原子存储使用 mo_release
.
如果第二个存储不是原子的,则任何 reader 都不可能安全地与任何东西同步,从而可以在没有数据争用 UB 的情况下观察值。
(尽管您在破解使用普通非 atomic
对象的 SeqLock 时 运行 进入发布 fence 的用例有效负载,以允许编译器进行优化。但这是一种特定于实现的行为,取决于了解 std::atomic 东西如何为真实 CPU 编译。例如,参见 。)
我对std::memory_order_acquire
和std::memory_order_release
的理解如下:
Acquire 表示在之后 acquire fence 出现的内存访问不能被重新排序到fence 之前。
Release表示在释放栅栏之前出现的内存访问不能被重新排序到栅栏之后。
我不明白的是,为什么特别是对于 C++11 原子库,获取栅栏与加载操作相关联,而释放栅栏与存储操作相关联。
澄清一下,C++11 <atomic>
库允许您以两种方式指定内存栅栏:要么您可以将栅栏指定为原子操作的额外参数,例如:
x.load(std::memory_order_acquire);
或者你可以使用std::memory_order_relaxed
并单独指定围栏,如:
x.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
我不明白的是,鉴于上述获取和释放的定义,为什么C++11特意将acquire与load[=关联起来53=],以及 release with store?是的,我看过许多示例,这些示例展示了如何使用 acquire/load 和 release/store 在线程之间进行同步,但总的来说,获取栅栏的想法(防止内存重新排序之后语句)和释放栅栏(防止语句之前的内存重新排序)与加载和存储的想法正交。
那么,为什么编译器不让我说:
x.store(10, std::memory_order_acquire);
我意识到我可以通过使用 memory_order_relaxed
,然后使用单独的 atomic_thread_fence(memory_order_acquire)
语句来完成上述操作,但是同样,为什么我不能直接使用 memory_order_acquire
存储?
一个可能的用例可能是,如果我想确保某些存储,比如 x = 10
,发生在 之前 一些可能影响其他线程的其他语句执行.
假设我写了一些数据,然后我写了一个指示,表明数据现在已经准备好了。看到数据已准备好指示的任何其他线程都必须看到数据本身的写入。因此,先前的写入不能超过该写入。
假设我读到一些数据已准备就绪。看到数据准备就绪后,我发出的任何读取都必须发生。因此后续读取不能移动到该读取之后。
因此,当您执行同步写入时,您通常需要确保您之前执行的所有写入都对看到同步写入的任何人可见。当您执行同步读取时,您之后执行的任何读取通常都必须在同步读取之后进行。
或者,换句话说,一个acquire通常是读取你可以获取或访问资源,并且后续的读取和写入一定不能在它之前移动。一个版本通常是写你已经完成了资源,并且前面的写不能移动到它之后。
std::memory_order_acquire
fence 仅确保在 fence 之后的所有 load 操作不会在 fence 之前的任何 load 操作之前重新排序,因此 memory_order_acquire
不能 确保存储在执行加载后对其他线程可见。这就是为什么 memory_order_acquire
不支持 store 操作的原因,你可能需要 memory_order_seq_cst
来实现 store 的获取。
或者,您可以说
x.store(10, std::memory_order_releaxed);
x.load(std::memory_order_acquire); // this introduce a data dependency
确保所有货品在入店前不会重新订购。同样,围栏在这里不起作用。
此外,原子操作中的内存顺序可能比内存栅栏更便宜,因为它只确保相对于原子指令的顺序,而不是栅栏前后的所有指令。
有关详细信息,另请参阅 formal description and explanation。
(纠正问题早期部分错误的部分答案。
你给的acquire/release的定义对于fences是错误的;它们仅适用于获取 operations 和释放 operations,如 x.store(mo_release)
,而不是 std::atomic_thread_fence(mo_release)
.
Acquire means that no memory accesses which appear after the acquire fence can be reordered to before the fence. [wrong, would be correct for acquire operation]
Release means that no memory accesses which appear before the release fence can be reordered to after the fence. [wrong, would be correct for release operation]
它们不足以用于栅栏,这就是为什么 ISO C++ 对获取栅栏(阻止 LoadStore / LoadLoad 重新排序)和释放栅栏(LoadStore / StoreStore)具有更强的排序规则。
当然,ISO C++ 没有定义“重新排序”,这意味着您正在访问一些全局一致的状态。改为 ISO C++
Jeff Preshing 的文章与此处相关:
- Acquire and Release Semantics(获取/释放 操作,例如加载、存储和 RMW)
- Acquire and Release Fences Don't Work the Way You'd Expect 解释了为什么那些单向屏障定义不正确且不足以用于围栏,这与操作不同。 (因为它会让围栏一直重新排序到程序的一端,并使所有操作彼此无序,因为它与操作本身无关。)
A possible use case for this might be if I want to ensure that some store, say x = 10, happens before some other statement executes that might affect other threads.
如果“其他语句”是来自原子共享变量的加载,您实际上需要 std::memory_order_seq_cst
来避免 StoreLoad 重新排序。 acquire
/ release
/ acq_rel
不会阻止它。
如果你的意思是确保原子存储在其他原子存储之前可见,正常的方法是让 2nd 原子存储使用 mo_release
.
如果第二个存储不是原子的,则任何 reader 都不可能安全地与任何东西同步,从而可以在没有数据争用 UB 的情况下观察值。
(尽管您在破解使用普通非 atomic
对象的 SeqLock 时 运行 进入发布 fence 的用例有效负载,以允许编译器进行优化。但这是一种特定于实现的行为,取决于了解 std::atomic 东西如何为真实 CPU 编译。例如,参见