std::mutex 的发布-获取可见性保证是否仅适用于关键部分?

Do the release-acquire visibility guarantees of std::mutex apply to only the critical section?

本文关键字:适用于 是否 键部 可见性 mutex std 获取      更新时间:2023-10-16

我试图理解标题下的这些部分 发布-获取排序https://en.cppreference.com/w/cpp/atomic/memory_order

他们说关于原子负载和存储:

如果线程 A 中的原子存储被标记为 memory_order_release 和 标记了来自同一变量的线程 B 中的原子负载 memory_order_acquire,所有内存写入(非原子和放松) 原子)发生-从原子存储之前从角度来看 线程 A,在线程 B 中成为可见的副作用。也就是说,一次 原子加载完成,线程B保证看到一切 线程 A 写入内存。

然后关于互斥体:

互斥锁,如 std::mutex 或原子自旋锁,是一个 释放-获取同步的示例:释放锁时 通过线程 A 和线程 B 获取,发生的一切 线程 A 上下文中的关键部分(发布之前) 必须对正在执行的线程 B(获取后)可见 相同的关键部分。

第一段似乎说,一个原子加载和存储(用memory_order_releasememory_order_acquire)线程B保证看到线程A写入的所有内容。 包括非原子写入。

第二段似乎表明互斥锁的工作方式相同,只是B 可见的范围仅限于关键部分中包装的任何内容,这是一个准确的解释吗? 还是每次写入,即使是关键部分之前的那些,B也可以看到?

我认为关于互斥体的 cppreference 引用之所以以这种方式编写,是因为如果您使用互斥锁进行同步,则所有用于通信的共享变量都应始终在关键部分内访问。

2017年标准在4.7.1中说:

获取互斥锁的调用将在 包含互斥锁的位置。相应地,一个释放的呼叫 相同的互斥锁将对这些互斥锁执行释放操作 地点。非正式地,对 A 力执行释放操作 先前对其他内存位置的副作用变得可见 稍后在其上执行消耗或获取操作的其他线程 一个。

更新:我想确保我有一个可靠的帖子,因为在网络上很难找到这些信息。感谢@Davis Herring为我指出正确的方向。

标准说

在33.4.3.2.11和33.4.3.2.25中:

互斥锁解锁与后续锁定操作同步,这些操作获得 同一对象的所有权

(https://en.cppreference.com/w/cpp/thread/mutex/lock、https://en.cppreference.com/w/cpp/thread/mutex/unlock)

4.6.16中:

与全表达式相关的每个值计算和副作用在与

要评估的下一个全表达式相关的每个值计算和副作用之前进行排序

https://en.cppreference.com/w/cpp/language/eval_order

4.7.1.9中:

一个评估 A线程间发生在评估 B 之前,如果

4.7.1.9.1) -- A 与 B 同步,或

4.7.1.9.2) -- A 在 B 之前按依赖顺序排序,或

4.7.1.9.3) -- 用于某些评估 X

4.7.1.9.3.1) ------ A 与 X 同步,X 在 B 之前排序,或

4.7.1.9.3.2) ------ A 在 X 和 X 线程间在 B 之前发生排序,或

4.7.1.9.3.3) ------ 线程间发生在 X 之前,X 线程间发生在 B 之前。

https://en.cppreference.com/w/cpp/atomic/memory_order

  • 因此,在 4.7.1.9.1 之前,互斥解锁 B 线程发生在后续锁 C之前
  • 在互斥锁解锁 B 之前按程序顺序发生的任何评估 A 也会在 C之前发生4.7.1.9.3.2
  • 因此,在unlock()保证所有以前的写入,即使是关键部分之外的写入,都必须对匹配的lock()可见。

这个结论与今天(和过去)实现互斥锁的方式一致,因为所有程序顺序的先前加载和存储都是在解锁之前完成的。(更准确地说,当任何线程中的匹配锁定操作观察到解锁时,存储必须可见。毫无疑问,这是理论上和实践中公认的释放定义。

这里没有魔法:互斥锁部分只是描述常见情况,其中(因为每次访问关键部分都可能写入共享数据)有问题的编写者使用互斥锁保护其所有访问。 (其他较早的写入是可见的,并且可能是相关的:考虑在不同步的情况下创建和初始化对象,然后将其地址存储在关键部分的共享变量中。

第一段似乎说原子加载和存储(与 memory_order_release,memory_order_acquire)线程B保证 查看线程 A 编写的所有内容。包括非原子写入。

不仅仅是写入,所有内存操作都已完成;您可以看到读取也已完成:虽然读取当然不会产生副作用,但您可以看到在发布之前读取永远不会看到在获取之后写入的值。

https://en.cppreference.com/都坚持写入(易于解释),完全忽略了正在完成读取的问题。

第二段似乎暗示互斥锁的工作方式相同, 除了对 B 可见的范围仅限于任何 包裹在关键部分,这是一个准确的解释吗? 或者每次写作,即使是在关键部分之前的那些 对 B 可见?

"在关键部分"甚至不是一回事。您执行的任何操作都无法与完成操作的内存状态分开。当您在"关键部分"设置整数对象时,该对象必须存在;将"写入对象"视为隔离是没有意义的,因为没有对象可谈。严格解释,"关键部分"将仅涵盖在其中创建的对象。但是这些对象都不会被其他线程知道,因此没有什么需要保护的。

因此,"关键部分"的结果本质上是程序的整个历史记录,对共享对象的某些访问仅在互斥锁之后开始。