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

31

我对 std::memory_order_acquirestd::memory_order_release 的理解如下:

Acquire 的意思是,在 acquire fence 之后的内存访问不能被重排序到 fence 之前。

Release 的意思是,在 release fence 之前的内存访问不能被重排序到 fence 之后。

然而,我不理解为什么在 C++11 原子库中,acquire fence 与 load 操作相关联,而 release fence 与 store 操作相关联。

需要澄清的是,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相关联,并将releasestore相关联?是的,我看过许多示例,展示了如何使用acquire/load与release/store之间进行线程同步,但总体而言,似乎acquire fences(防止语句后内存重排序)和release fences(防止语句前内存重排序)的概念与loads和stores的概念是正交的。
那么,例如,为什么编译器不允许我这样说:
x.store(10, std::memory_order_acquire);

我意识到我可以通过使用 memory_order_relaxed,然后分别使用一个单独的 atomic_thread_fence(memory_order_acquire) 语句来实现上述目标,但是,为什么不能直接使用带有 memory_order_acquire 的存储呢?

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


4
在典型的无锁算法中,你会读取一个原子变量来判断共享资源是否可以被使用(即准备好被获取),并且你会写入一个原子变量来表明该共享资源已经可以被使用(即释放资源)。你不希望在检查守卫共享资源的原子操作之前,共享资源的读取操作被执行;同样,你也不希望在标志释放操作完成的原子操作之后,对于即将共享的资源进行初始化。 - Igor Tandetnik
在这个例子中,只有 atomic_thread_fence(std::memory_order_acquire) 是一个真正的栅栏。请参阅标准中的 **1.10:5 Multi-threaded executions and data races [intro.multithread]**,该标准规定(引用草案n3797):“没有关联内存位置的同步操作是一个栅栏,可以是获取栅栏、释放栅栏或获取和释放栅栏。”相比之下,x.load(std::memory_order_acquire) 是一个原子操作,它对 x 执行了一个获取操作,如果该值与存储到 x 的释放操作匹配,则它将成为一个同步操作。 - amdn
在引言中,标准(草案n3797)并不将获取操作限制为加载操作,也不将释放操作限制为存储操作。这是不幸的。您必须转到第29.3条:1序和一致性[原子.order]才能找到“memory_order_acquire,memory_order_acq_rel和memory_order_seq_cst:加载操作对受影响的内存位置执行获取操作”和“memory_order_release,memory_order_acq_rel和memory_order_seq_cst:存储操作对受影响的内存位置执行释放操作”。 - amdn
@amdn 但是即使是“真正的栅栏”也不必总是产生CPU栅栏;它与先前或后续原子操作交互以产生某种效果。只有非常幼稚的编译器才会将给定的CPU指令与“真正的栅栏”的每个源代码出现关联起来。 - curiousguy
我刚刚发现了这篇文章,它用很好的例子描述了获取/释放同步的概念,对于那些对此感到困惑的人可能会有所帮助。 - Ham
显示剩余2条评论
3个回答

35
假设我写入一些数据,然后写入一个指示表明数据现在已经准备好的标记。任何看到数据已准备好标记的其他线程都不能看到数据本身的写入。因此,在该写入之前的写入不能超越该写入。
假设我读取某些数据已经就绪。任何在看到这种状态后发出的读取必须在看到已准备好数据的读取之后进行。因此,随后的读取不能落后于该读取。
因此,当您进行同步写入时,通常需要确保所有在其之前进行的写入对任何看到同步写入的人都可见。当您进行同步读取时,通常必须确保在同步读取之后进行的任何读取都是在其后进行的。
或者,换句话说,Acquire(获取)通常表示可以获取或访问资源,并且随后的读取和写入不能被移动到其之前。Release(释放)通常表示完成了资源的使用,先前的写入不能被移动到其之后。

5

(纠正问题早期的错误部分。 David Schwartz 的回答 已经很好地回答了你正在问的主要问题。Jeff Preshing 的“Acquire / Release”文章也是另一个视角的好读物。)


你提供的 acquire/release 的定义对于 fences 是错误的;它们仅适用于 acquire 操作和 release 操作,例如 x.store(mo_release),而不是std::atomic_thread_fence(mo_release)

  • Acquire 表示出现在 acquire fence 之后的任何内存访问都不能被重新排序到 fence 之前。[错误,对于 acquire operation 是正确的]

  • Release 表示出现在 release fence 之前的任何内存访问都不能被重新排序到 fence 之后。[错误,对于 release operation 是正确的]

它们对于 fences 来说是不足的,这就是为什么 ISO C++ 对于 acquire fence(阻止 LoadStore/LoadLoad 重排序)和 release fence(LoadStore/StoreStore)有更强的排序规则。

当然,ISO C++ 没有定义“重排序”,因为这将意味着你正在访问某种全局一致状态。相反,ISO C++

Jeff Preshing 的文章在此处是相关的:


这可能是一个使用情况,如果我想确保一些存储,比如 x = 10,在另一些语句执行之前发生,这些语句可能会影响其他线程。

如果“其他语句”是对原子共享变量的 load,则实际上需要std::memory_order_seq_cst来避免 StoreLoad 重排序。acquire / release / acq_rel 不会阻塞它。

如果您想确保某个原子存储在另一个原子存储之前可见,通常的方法是让第个原子存储使用mo_release

如果第2个存储不是原子性的,任何读取器都无法安全地与其同步,以便观察值而不会出现数据竞争UB。

(虽然您在为负载使用普通非atomic对象的SeqLock上进行hack时遇到了使用release fence的用例,以允许编译器优化。 但这是一种依赖于知道std :: atomic东西如何编译为真实CPU的具体实现行为。有关示例,请参见实现具有32位原子的64位原子计数器。)


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

-4

std::memory_order_acquire栅栏只确保在栅栏之后的所有加载操作不会被重新排序到栅栏之前的任何加载操作之前,因此memory_order_acquire 不能确保存储对其他线程可见,当执行后续加载时。这就是为什么memory_order_acquire不支持存储操作,您可能需要memory_order_seq_cst来实现存储的获取。

作为替代方案,您可以说

x.store(10, std::memory_order_releaxed);
x.load(std::memory_order_acquire);  // this introduce a data dependency

为了确保所有的负载在存储之前不会被重新排序,需要使用内存栅栏。但是,在这里内存栅栏无法发挥作用。
此外,原子操作中的内存顺序可能比内存栅栏更便宜,因为它仅确保相对于原子指令的顺序,而不是在栅栏之前和之后的所有指令的顺序。
有关详细信息,请参见正式描述解释

1
第一句话不太准确(-1)。 实际上,任何紧随acquire fence的内存访问都不能与在该fence之前的任何load操作重新排序。(相反,任何先于release fence的内存访问都不能与在该fence之后的任何store操作重新排序。) - John Wickerson
@JohnWickerson,实际上,“memory_order_releaxed”仅确保在任何原子操作或使用“memory_order_release”的fence之后发生的加载才会发生。它不为fence之后的存储提供任何顺序。请参见atomic_thread_fence中的原子-fence同步部分。 - user1887915
有趣!我相信你提到的cppreference.com网站在这里实际上是错误的。根据官方的C11标准,release和acquire fences的行为方式就像我描述的那样。 - John Wickerson
如果您感兴趣,我在我的博客上写了更多关于这个问题的内容:https://johnwickerson.wordpress.com/2016/08/11/cppreference-acquirerelease-wrong/ - John Wickerson
我有一个关于这个答案中提到的代码的问题。我认为这里的代码没有任何意义,因为“x.store”操作本身可以在获取屏障之后重新排序。所以,即使获取屏障后的负载可能不会在获取屏障之前重新排序,但存储本身也可以在获取之后进行,对吧? - tabs_over_spaces
1
@Aditya 存储并加载同一原子变量(在同一线程中)不能被重新排序。 - user1887915

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接