c++11 及更高版本中 mutex.lock() 和 .unlock() 的确切线程间重新排序约束是什么?

Fli*_*pvs 7 c++ mutex memory-barriers language-lawyer c++11

根据https://en.cppreference.com/w/cpp/atomic/memory_order mutex.lock()mutex.unlock()是获取和释放操作。获取操作使得无法在它前面重新排序后面的指令。并且释放操作使得在它之后无法重新排序之前的指令。这使得以下代码:

[Thread 1]
mutex1.lock();
mutex1.unlock();
mutex2.lock();
mutex2.unlock();
[Thread 2]
mutex2.lock();
mutex2.unlock();
mutex1.lock();
mutex1.unlock();
Run Code Online (Sandbox Code Playgroud)

可以重新排序为以下(可能是死锁)代码:

[Thread 1]
mutex1.lock();
mutex2.lock();
mutex1.unlock();
mutex2.unlock();
[Thread 2]
mutex2.lock();
mutex1.lock();
mutex2.unlock();
mutex1.unlock();
Run Code Online (Sandbox Code Playgroud)

这种重新排序是否可能发生。或者有什么规则阻止它?

Pet*_*des 4

几乎重复:C++ 标准如何使用 memory_order_acquire 和 memory_order_release 防止自旋锁互斥体中的死锁?- 这是使用手卷自旋std::atomic锁,但同样的推理也适用:

编译器无法以可能导致死锁的方式对互斥体获取和释放进行编译时重新排序,而 C++ 抽象机没有死锁。 这将违反假定规则。
它实际上会在源没有的地方引入无限循环,违反了这一规则:

ISO C++ 当前草案,第 6.9.2.3 节向前进展

18.实现应确保原子或同步操作分配的最后一个值(按修改顺序)将在有限的时间内对所有其他线程可见


ISO C++ 标准不区分编译时重新排序和运行时重新排序。 事实上,它没有说任何关于重新排序的事情。它只说明了由于同步效果、每个原子对象的修改顺序的存在以及 seq_cst 操作的总顺序而保证您何时看到某些内容。这是对标准的误读,将其视为允许以一种要求互斥体以与源顺序不同的顺序将事物钉入汇编中的许可

获取互斥体本质上相当于memory_order_acquire互斥体对象上的原子 RMW。(事实上​​,ISO C++ 标准甚至将它们分组在上面引用的 6.9.2.3 :: 18 中。)

您可以看到早期版本或宽松存储甚至 RMW 出现在互斥锁/解锁关键部分内而不是之前。但该标准要求原子存储(或同步操作)立即对其他线程可见,因此编译时重新排序以强制其等待直到获取锁之后可能会违反及时性保证。因此,即使是宽松的存储也无法使用 a 进行编译时/源代码级重新排序mutex.lock(),只能作为运行时效果。

同样的推理也适用于mutex2.lock(). 您可以看到重新排序,但编译器无法创建代码要求始终发生重新排序的情况,如果这使得执行在任何重要/长期可观察的方面与 C++ 抽象机不同。(例如,围绕无限等待重新排序)。制造死锁算作其中一种方式,无论是出于这个原因还是其他原因。(每个理智的编译器开发人员都会同意这一点,即使 C++ 没有正式语言来禁止它。)

请注意,互斥体解锁不能阻塞,因此因此不会禁止两个解锁的编译时重新排序。(如果中间没有缓慢或可能阻塞的操作)。 但互斥锁解锁是一个“释放”操作,因此排除了这种情况:两个释放存储不能相互重新排序。


顺便说一句,防止 mutex.lock() 操作在编译时重新排序的实用机制只是使它们成为编译器不知道如何内联的常规函数​​调用。它必须假设函数不是“纯粹的”,即它们对全局状态有副作用,因此顺序可能很重要。这与将操作保留在关键部分内的机制相同:互斥锁定和解锁函数如何防止 CPU 重新排序?

用 std::atomic 编写的内联 std::mutex最终取决于编译器实际应用的规则,这些规则使操作立即可见,并且不会通过在编译时重新排序而引入死锁。如C++ 标准如何使用 memory_order_acquire 和 memory_order_release 防止自旋锁互斥体中的死锁?