std::mutex 的 release-acquire 可见性保证是否仅适用于临界区?

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

我正在尝试理解 Release-Acquire 排序 https://en.cppreference.com/w/cpp/atomic/memory_order

标题下的这些部分

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

If an atomic store in thread A is tagged memory_order_release and an atomic load in thread B from the same variable is tagged memory_order_acquire, all memory writes (non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B. That is, once the atomic load is completed, thread B is guaranteed to see everything thread A wrote to memory.

然后关于互斥:

Mutual exclusion locks, such as std::mutex or atomic spinlock, are an example of release-acquire synchronization: when the lock is released by thread A and acquired by thread B, everything that took place in the critical section (before the release) in the context of thread A has to be visible to thread B (after the acquire) which is executing the same critical section.

第一段似乎说原子加载和存储(使用memory_order_releasememory_order_acquire)线程B保证看到一切线程A写了。包括 non-atomic 写入。

第二段似乎表明互斥体的工作方式相同,除了 B 可见的范围仅限于关键部分中包含的内容,是那一个准确的解释?还是每次写入,甚至是临界区之前的写入对 B 都可见?

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

我认为关于互斥锁的 cppreference 引用之所以这样写是因为如果您使用互斥锁进行同步,则应始终在临界区内访问用于通信的所有共享变量。

2017标准在4.7.1中说:

a call that acquires a mutex will perform an acquire operation on the locations comprising the mutex. Correspondingly, a call that releases the same mutex will perform a release operation on those same locations. Informally, performing a release operation on A forces prior side effects on other memory locations to become visible to other threads that later perform a consume or an acquire operation on A.

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

标准说

33.4.3.2.1133.4.3.2.25:

mutex unlock synchronizes with subsequent lock operations that obtain ownership on the same object

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

4.6.16:

Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.

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

4.7.1.9:

An evaluation A inter-thread happens before evaluation B if

4.7.1.9.1) -- A synchronizes-with B, or

4.7.1.9.2) -- A is dependency-ordered before B, or

4.7.1.9.3) -- for some evaluation X

4.7.1.9.3.1) ------ A synchronizes with X and X is sequenced before B, or

4.7.1.9.3.2) ------ A is sequenced before X and X inter-thread happens before B, or

4.7.1.9.3.3) ------ A inter-thread happens before X and X inter-thread happens before B.

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

  • 所以互斥解锁 B 线程间发生在 4.7.1.9.1 的后续锁 C 之前。
  • 在互斥体解锁 B 之前按程序顺序发生的任何求值 A 也 线程间发生在 C 之前 C by 4.7.1.9.3.2
  • 因此,在 unlock() 保证之前的所有写入,甚至那些在临界区之外的写入,都必须对匹配的 lock() 可见。

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

The first paragraph seems to say that an atomic load and store (with memory_order_release, memory_order_acquire) thread B is guaranteed to see everything thread A wrote. including non-atomic writes.

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

所有https://en.cppreference.com/都坚持写(很容易解释)而完全忽略了读完成的问题。

The second paragraph seems to suggest that a mutex works the same way, except the scope of what is visible to B is limited to whatever was wrapped in the critical section, is that an accurate interpretation? or would every write, even those before the critical section be visible to B?

但是"in the critical section"甚至都不是东西。你所做的任何事情都不能脱离它完成时的记忆状态。当你设置一个整型对象"in the critical section"时,这个对象必须存在;将 "write to an object" 视为孤立是没有意义的,因为没有任何对象可以谈论。严格解释,"the critical section" 将仅涵盖在其中创建的对象。但是这些对象中的 none 会被其他线程知道,因此没有什么需要保护的。

所以"critical section"的结果本质上是程序的整个历史,一些对共享对象的访问仅在互斥锁之后开始。