内存栅栏:获取/加载和释放/存储

Posted

技术标签:

【中文标题】内存栅栏:获取/加载和释放/存储【英文标题】:Memory fences: acquire/load and release/store 【发布时间】:2016-08-17 21:55:44 【问题描述】:

我对@9​​87654321@和std::memory_order_release的理解如下:

Acquire 表示在获取围栏之后出现的任何内存访问都不能重新排序到围栏之前。

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);

我不明白的是,鉴于上述acquire和release的定义,为什么C++11专门将acquireload联系起来,商店一起发布?是的,我已经看到了许多示例,这些示例展示了如何使用获取/加载和释放/存储来在线程之间同步,但总的来说,获取围栏(防止语句后内存重新排序)和释放的想法似乎是栅栏(在语句之前防止内存重新排序)与加载和存储的想法是正交的。

那么,例如,为什么编译器不让我说:

x.store(10, std::memory_order_acquire);

我意识到我可以通过使用memory_order_relaxed,然后使用单独的atomic_thread_fence(memory_order_acquire) 语句来完成上述操作,但是,为什么我不能直接使用带有memory_order_acquire 的store?

如果我想确保某个存储(例如 x = 10)发生在其他可能影响其他线程的语句执行之前,可能会出现这种情况。

【问题讨论】:

在典型的无锁算法中,你读取一个 atomic 以查看共享资源是否已准备好使用(准备好被获取),然后编写一个 atomic 以指示共享资源已准备好被使用(释放资源)。您不希望在检查原子保护之前移动共享资源的读取;并且您不希望待共享资源的初始化在写入原子后移动,表示释放。 在示例中,只有atomic_thread_fence(std::memory_order_acquire) 是真正的栅栏。请参阅标准中的 1.10:5 多线程执行和数据竞争 [intro.multithread],其中说(引用草案 n3797)“没有关联内存位置的同步操作是栅栏并且可以是获取栅栏、释放栅栏,或者同时是获取和释放栅栏。” 相比之下,x.load(std::memory_order_acquire) 是一个原子操作,它执行获取 对x 的操作,如果该值与将 release 存储到 x 中匹配,则它将是一个 同步操作 在简介中,标准(n3797 草案)并未将获取操作限制为加载操作,并将释放操作限制为存储操作。那是不幸的。您必须转到子句 29.3:1 顺序和一致性 [atomics.order] 才能找到 “memory_order_acquire、memory_order_acq_rel 和 memory_order_seq_cst:加载操作对受影响的内存位置执行获取操作” and “memory_order_release, memory_order_acq_rel, and memory_order_seq_cst:存储操作对受影响的内存位置执行释放操作” @amdn 但即使是“真正的栅栏”也根本不需要产生 CPU 栅栏;它与先前或随后的原子操作交互以产生一些效果。只有非常幼稚的编译器才会将给定的 CPU 指令与“真正的栅栏”的每个源代码出现相关联。 与加载和存储的概念正交” 在原子语义下,读取甚至不是修改顺序中的有序事件。您需要写信才能进入该订单;即使你总是写完全相同的值,完全相同值的写入也是有序的。然后您在修改顺序中谈到该写入事件之后。 (从物理上讲,这意味着缓存已经占用了缓存行。)但是释放读取将是模棱两可的,因为相同写入事件的其他读取没有排序。您会更改语义以在修改顺序中包含读取吗? 【参考方案1】:

假设我写了一些数据,然后我写了一个数据现在准备好的指示。当务之急是没有其他线程看到数据准备就绪的指示,而不会看到数据本身的写入。所以之前的写入不能超过那个写入。

假设我读到一些数据已经准备好了。在看到数据已准备好的读取之后,我发布的任何读取都必须发生。所以后续读取不能落后于该读取。

因此,当您执行同步写入时,您通常需要确保您之前执行的所有写入对于看到同步写入的任何人都是可见的。当您执行同步读取时,通常必须在同步读取之后执行您之后执行的任何读取。

或者,换句话说,获取通常是您可以获取或访问资源的读取,并且后续读取和写入不得移动到它之前。发布通常是写你已经完成了资源,并且之前的写不能移到它之后。

【讨论】:

【参考方案2】:

std::memory_order_acquire栅栏只确保栅栏之后的所有加载操作不会在栅栏之前的任何加载操作之前重新排序,因此memory_order_acquire不能 em> 确保在执行加载后存储对其他线程可见。这就是为什么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 和 explanation。

【讨论】:

第一句话不太对(-1)。实际上,在获取栅栏之后的任何内存访问都不能使用该栅栏之前的任何加载操作重新排序。 (相反,释放栅栏之前的任何内存访问都不能使用该栅栏之后的任何存储操作重新排序。) @JohnWickerson 实际上memory_order_releaxed 仅确保在任何原子操作或memory_order_release 的栅栏发生后栅栏发生后加载。它不提供围栏后的任何商店订购。请参阅atomic_thread_fence 中的 atomic-fence 同步部分 有趣!我相信您所指的 cppreference.com 网站实际上是错误的。根据官方 C11 标准,发布和获取栅栏的行为方式与我描述的方式相同。 如果你有兴趣,我已经在我的博客上写了更多关于这个问题的内容:johnwickerson.wordpress.com/2016/08/11/… @Aditya 存储和加载到同一个原子变量(在同一个线程中)无法重新排序。【参考方案3】:

(部分答案纠正了问题前半部分的错误。David Schwartz's answer 已经很好地涵盖了您要问的主要问题。Jeff Preshing 的 article on acquire / release 也很适合阅读其他内容。)


您为获取/释放提供的定义对于栅栏是错误的;它们仅适用于获取操作和释放操作,例如x.store(mo_release),而不是std::atomic_thread_fence(mo_release)

Acquire 意味着在获取栅栏之后出现的任何内存访问都不能重新排序到栅栏之前。 [错误,获取操作是正确的]

释放意味着在释放栅栏之前出现的任何内存访问都不能重新排序到栅栏之后。 [错误,对于发布操作是正确的]

它们对于栅栏来说是不够的,这就是为什么 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 解释了为什么这些单向障碍定义不正确且不足以用于栅栏,这与操作不同。 (因为它会让栅栏一直重新排序到程序的一端,并使所有操作彼此无序,因为它与操作本身无关。)

如果我想确保某个存储(例如 x = 10)发生在可能影响其他线程的其他语句执行之前,可能会出现这种情况。

如果“其他语句”是来自原子共享变量的加载,您实际上需要std::memory_order_seq_cst 以避免 StoreLoad 重新排序。 acquire / release / acq_rel 不会阻止它。

如果您的意思是确保原子存储在其他原子存储之前可见,通常的方法是使 2nd 原子存储使用mo_release

如果第二个存储不是原子的,那么任何读取器都不太可能安全地与任何东西同步,以在没有数据竞争 UB 的情况下观察值。

(虽然您在破解使用普通非atomic 对象作为有效负载的 SeqLock 时确实遇到了发布fence的用例,以允许编译器进行优化。但那是一种特定于实现的行为,取决于了解 std::atomic 内容如何为真实 CPU 编译。例如,请参阅 Implementing 64 bit atomic counter with 32 bit atomics。)

【讨论】:

我应该提供 Jeff 的帖子作为评论,而不是纯链接的答案。但实际上,有这个好答案就更棒了。所以谢谢你,我的答案可以隐藏:)

以上是关于内存栅栏:获取/加载和释放/存储的主要内容,如果未能解决你的问题,请参考以下文章

内存栅栏

如何在android中使用imageloader释放位图内存?

今日刷题总结21

如何释放手机内存 清理释放安卓手机内存空间方法

触发释放内存的释放?

是否应该为互斥锁获取-交换循环(或队列获取-加载循环)组合内存栅栏,还是应该避免?