微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

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

根据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();

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

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

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

解决方法

几乎是重复的:How C++ Standard prevents deadlock in spinlock mutex with memory_order_acquire and memory_order_release? - 使用手动滚动的 std::atomic 自旋锁,但同样的推理适用:

编译器无法在编译时重新排序互斥锁的获取和释放,这可能会导致 C++ 抽象机没有的死锁。 这会违反 as-if 规则。
这将有效地在源没有的地方引入无限循环,违反此规则:

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

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


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

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

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

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

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


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

用 std::atomic 编写的可内联 std::mutex 最终会取决于编译器实际应用有关使操作立即可见的规则,而不是通过在编译时重新排序来引入死锁。如How C++ Standard prevents deadlock in spinlock mutex with memory_order_acquire and memory_order_release?

中所述 ,

acquire 操作使得它前面的后续指令无法重新排序。并且发布操作使得在它之后无法重新排序之前的指令。

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。