在 C++11 线程中,std::mutex 对内存可见性有什么保证?

In C++11 threads, what guarantees does a std::mutex have about memory visibility?

本文关键字:可见性 什么 内存 mutex 线程 C++11 std      更新时间:2023-10-16

我目前正在尝试学习 C++11 线程 api,我发现各种资源没有提供基本信息:如何处理 CPU 缓存。现代 CPU 的每个内核都有一个缓存(这意味着不同的线程可能使用不同的缓存(。这意味着一个线程可能将值写入内存,而另一个线程看不到它,即使它看到第一个线程也进行了其他更改。

当然,任何好的线程 API 都提供了一些方法来解决这个问题。然而,在C++的线程 API 中,尚不清楚它是如何工作的。我知道std::mutex,例如,以某种方式保护内存,但不清楚它的作用:它是清除整个 CPU 缓存,还是仅清除当前线程缓存中互斥锁内访问的对象,还是其他什么?

此外,显然,只读访问不需要互斥锁,但是如果线程 1(并且只有线程 1(不断写入内存以修改对象,那么其他线程的潜力是否会看到该对象的过时版本,从而使某种缓存清除成为必要?

原子类型是否只是绕过缓存并使用单个 CPU 指令从主内存读取值?他们是否对访问内存中的其他位置做出任何保证?

在 CPU 缓存的上下文中,C++11 线程 API 中的内存访问如何工作?

一些问题,例如这个关于内存围栏和内存模型的问题,但似乎没有来源在 CPU 缓存的上下文中解释这一点,这就是这个问题所要求的。

std::mutex具有release-acquire内存排序语义,因此线程 A 中从线程 A 的角度happened-before原子写入关键部分的所有内容都必须对线程 B 可见,然后才能进入线程 B 中的关键部分。

阅读 http://en.cppreference.com/w/cpp/atomic/memory_order 以开始使用。另一个很好的资源是C++并发在行动》一书。话虽如此,在使用高级同步原语时,您应该能够忽略大多数这些细节,除非您好奇或想弄脏自己的手。

我想我明白你在说什么。这里有三件事在起作用。

  • C++11标准描述了语言级别发生的事情...锁定std::mutex是一种同步操作。C++标准没有描述它是如何工作的。就C++标准而言,不存在 CPU 缓存。

  • C++实现在某些时候会在应用程序中放置一些实现互斥锁的机器代码。创建此实现的工程师必须同时考虑 C++11 规范和体系结构规范。

  • CPU 本身以提供C++实现工作所需的语义的方式管理缓存。

如果您查看原子组学,这可能更容易理解,它转化为更小的汇编代码片段,但仍提供同步。例如,在GodBolt上尝试这个:

#include <atomic>
std::atomic<int> value;
int acquire() {
return value.store(std::memory_order_acquire);
}
void release() {
value.store(0, std::memory_order_release);
}

您可以看到程序集:

acquire():
mov eax, DWORD PTR value[rip]
ret
release():
mov DWORD PTR value[rip], 0
ret
value:
.zero 4

所以在 x86 上,没有什么必要的,CPU 已经提供了所需的内存排序语义(尽管您可以使用显式mfence它通常由操作暗示(。这绝对不是它在所有处理器上的工作方式,请参阅电源输出:

acquire():
.LCF0:
0: addis 2,12,.TOC.-.LCF0@ha
addi 2,2,.TOC.-.LCF0@l
addis 3,2,.LANCHOR0@toc@ha # gpr load fusion, type int
lwz 3,.LANCHOR0@toc@l(3)
cmpw 7,3,3
bne- 7,$+4
isync
extsw 3,3
blr
.long 0
.byte 0,9,0,0,0,0,0,0
release():
.LCF1:
0: addis 2,12,.TOC.-.LCF1@ha
addi 2,2,.TOC.-.LCF1@l
lwsync
li 9,0
addis 10,2,.LANCHOR0@toc@ha
stw 9,.LANCHOR0@toc@l(10)
blr
.long 0
.byte 0,9,0,0,0,0,0,0
value:
.zero 4

这里有明确的isync指令,因为没有它们,电源存储器模型提供的保证较少。

然而,这只是将事情降低到较低的水平。CPU 本身使用 MESI 协议等技术管理共享缓存,这是一种维护缓存一致性的技术。

在 MESI 协议中,当内核修改缓存块时,它必须从其他缓存中刷新该块。其他内核将块标记为无效,并在必要时将内容写入主存储器。这是低效的,但是必不可少的。出于这个原因,您不想尝试将一堆常用的互斥体或原子变量推到内存的小区域中,因为您最终可能会让多个内核争夺同一个缓存块。维基百科的文章相当全面,比我在这里写的更详细。

我省略的是,互斥体通常需要某种内核级别的支持才能使线程进入睡眠或唤醒状态。