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

Loc*_*yer 5 c++ multithreading mutex memory-model thread-safety

我试图在标题Release-Acquire ordering 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获取时,在临界区中发生的所有事情(释放之前)在执行相同关键部分的线程B(在获取之后)必须对线程A上下文可见。

第一款似乎是说,一个原子负载和存储(含memory_order_releasememory_order_acquire)线程B保证看到的一切线程A写道。包括非原子写入。

第二段似乎暗示了互斥锁的工作方式相同,只是 B可见的范围仅限于关键部分中包装的内容,这是否是正确的解释?还是每次写,甚至关键部分之前的内容都对B可见?

Hum*_*ago 5

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

2017年标准4.7.1中说:

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

因此,unlock上一个持有锁的线程中之前的所有内容都发生在下lock一个获取锁的线程中之后的所有内容之前。

这种跨线程链接,每个线程都获取锁,使得前一个锁持有者的操作对于后来的锁持有者以及它自己的操作都是可见的。


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

标准说

33.4.3.2.1133.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) ------ A 线程间发生在 X 之前,X 线程间发生在 B 之前。

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

  • 因此,互斥体解锁 B线程间发生在4.7.1.9.1 之前的后续锁定 C 之前。
  • 4.7.1.9.3.2在互斥体解锁 B 之前按程序顺序发生的任何评估 A 也发生在 C 之前
  • 因此,在unlock()保证所有先前的写入(即使是临界区之外的写入)之后,必须对匹配的 可见lock()

这个结论与现在(以及过去)互斥体的实现方式一致,因为所有程序顺序的先前加载和存储都是在解锁之前完成的。(更准确地说,当任何线程中的匹配锁定操作观察到解锁可见时,存储必须先可见。)毫无疑问,这是理论上和实践中公认的释放定义。(例如https://preshing.com/20120913/acquire-and-release-semantics/)。事实上,这就是为什么 acquirerelease具有这些名称,当推广到无锁原子时,从它们创建锁的起源开始。