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

What are the exact inter-thread reordering constraints on mutex.lock() and .unlock() in c++11 and up?

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

[Thread 1]
mutex1.lock();
mutex1.unlock();
mutex2.lock();
mutex2.unlock();
[Thread 2]
mutex2.lock();
mutex2.unlock();
mutex1.lock();
mutex1.unlock();

可以重新排序成如下(可能死锁)代码:

[Thread 1]
mutex1.lock();
mutex2.lock();
mutex1.unlock();
mutex2.unlock();
[Thread 2]
mutex2.lock();
mutex1.lock();
mutex2.unlock();
mutex1.unlock();

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

An acquire operation makes it impossible to reorder later instructions in front of it. And release operations make it impossible to reorder earlier instructions after it.

锁定互斥锁不是内存读取,不是内存写入,也不是指令。这是一种具有大量内部排序要求的算法。具有排序要求的操作本身使用某种机制来确保遵循该操作的要求,而不管它之前或之后发生的其他操作允许进行何种重新排序。

在考虑两个操作是否可以重排序时,必须同时遵守这两个操作的排序约束。互斥锁定和解锁操作包含许多内部操作,它们有自己的顺序约束。您不能只是移动操作块并假设您没有违反这些操作的内部约束。

如果您平台上的互斥锁锁定和解锁实现没有足够的顺序约束来确保它们在按预期使用时正常工作,那么这些实现将被破坏。

几乎是重复的: - 这是使用手动 std::atomic 自旋锁,但同样的推理适用:

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

ISO C++ current draft, section 6.9.2.3 Forward progress

18. An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.


ISO C++ 标准不区分编译时与 运行 时重新排序。 事实上它没有说明任何关于 重新订购。它只说明何时由于同步效果而保证您看到某些内容,以及每个原子对象的修改顺序的存在,以及 seq_cst 操作的总顺序。以 要求 互斥量以不同于源代码顺序的方式将其固定到 asm 中是对标准的误读。

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

您可以看到早期版本或宽松存储甚至 RMW 出现在互斥 lock/unlock 临界区内而不是之前。但是该标准要求其他线程能够立即看到原子存储(或同步操作),因此编译时重新排序 force 它等到获得锁之后可能会违反这一点及时性保证。因此,即使是轻松的存储也无法使用 mutex.lock() 进行编译时/源代码级别的重新排序,只能作为 运行 时间效果。

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

注意 mutex unlock 不能阻塞,所以编译时两个解锁的重新排序并没有因为这个原因被禁止。 (如果中间没有缓慢或可能阻塞的操作)。 但是互斥解锁是一个“释放”操作,所以排除了:两个释放存储不能相互重新排序。


顺便说一句,防止 mutex.lock() 操作在编译时重新排序的实用机制只是让它们成为编译器不知道如何内联的常规函数​​调用。它必须假设函数不是“纯”的,即它们对全局状态有副作用,因此顺序可能很重要。这与将操作保持在临界区内的机制相同:

用 std::atomic 编写的可内联 std::mutex 最终会 取决于编译器实际应用有关使操作迅速可见而不引入死锁的规则在编译时重新排序。如

中所述