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

Posted

技术标签:

【中文标题】c++11 及更高版本中 mutex.lock() 和 .unlock() 的确切线程间重新排序约束是啥?【英文标题】:What are the exact inter-thread reordering constraints on mutex.lock() and .unlock() in c++11 and up?c++11 及更高版本中 mutex.lock() 和 .unlock() 的确切线程间重新排序约束是什么? 【发布时间】:2021-07-02 08:27:17 【问题描述】:

根据https://en.cppreference.com/w/cpp/atomic/memory_ordermutex.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();

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

【问题讨论】:

这能回答你的问题吗? At what point is code reordering in C++ optimization stopped? @Jeffrey 不幸的是,这并没有回答这个问题。假设正确定义了 c++ 标准,“as-if”规则当然会成立。问题是对互斥操作的隐含获取/释放重新排序约束是否足以让这个“as-if”规则真正成立。据我所知,它没有。 可能是 cppreference 应用于排序保证的简化的产物。该标准要求在同一个互斥锁上解锁->锁定的同步关系,并且您可以从中派生与其他操作的线程间发生之前的关系。 我认为正常的顺序规则在这里适用? (mutex1.unlock() 会在mutex2.lock() 之前排序)memory_order 在多线程中不是最有用的吗? 如果我们展开负载并存储这些操作在现实生活中执行的操作,其中mutex2.lock() 涉及循环中的获取负载(或 rmw),直到锁可用,那么我们看到没关系如果其中一个或多个负载在mutex1.unlock() 的发布存储之前重新排序,只要该存储最终完成。它应该在任何可用的机器上执行。标准的语言是否正式保证这是另一个问题,但我确信他们希望这段代码能够工作而不是死锁。 【参考方案1】:

几乎是重复的: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?中所述

【讨论】:

即使它确实知道如何内联它们,并且即使锁类似于编译器可以理解的所有内容(没有不透明的系统调用),它也会看到 unlock() 涉及一个循环尝试锁定,直到它变得可用。该循环可能会迭代无限次,并且及时性保证应禁止编译器将存储重新排序到此类循环的另一端。 @NateEldredge:确实,这将是How C++ Standard prevents deadlock in spinlock mutex with memory_order_acquire and memory_order_release? 的完全复制品,并且取决于编译器了解原子并具有合理的优化规则,而不是通常调用不透明的互斥机制功能。 “所以编译时重新排序以强制它等到获得锁后可能会违反及时性保证。” 当我们在每个上替换第二个lock 时带有try_lock 的线程,我们避免了死锁/无限循环。这似乎意味着我们可以观察 try_lock 被重新排序到关键部分... 谢谢。规则“实现应确保由原子或同步操作分配的最后一个值(按修改顺序)将在有限的时间段内对所有其他线程可见。”如 P0062R1 中所述,确实应该防止这种感知到的重新排序发生。 @Flipvs:啊,谢谢,没有意识到同步操作包含在我为原子考虑的完全相同的点中。更新为链接+引用它。【参考方案2】:

获取操作使得无法重新排序前面的后续指令。并且发布操作使得在它之后重新排序之前的指令是不可能的。

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

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

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

【讨论】:

是的,但是在发布存储之后(按程序顺序)获取加载的情况下,两者都没有阻止交换顺序的排序约束。但在我看来,这里的问题是,为了产生死锁,我们实际上必须在发布存储之前重新排序 infinitely many 获取负载,以便发布存储无限期延迟,而这不是应该发生。 @NateEldredge 您不只是重新排序加载和存储,您正在重新排序整个算法,该算法在其本身使用的操作内部必须具有足够的排序约束,以确保其工作于预期目的(否则它会坏掉)。 我认为彼得的回答更好地说明了我试图提出的观点。 mutex1.unlock()mutex2.lock()的各个内部步骤如何相互重新排序并不重要,只要mutex1.unlock()“迅速”完成,这是一个单独的保证。

以上是关于c++11 及更高版本中 mutex.lock() 和 .unlock() 的确切线程间重新排序约束是啥?的主要内容,如果未能解决你的问题,请参考以下文章

为啥支持 C++11 及更高版本的 C++ 编译器需要 Boost.SmartPtr?

在 Java 11 及更高版本中使用 HttpClient 时如何跟踪 HTTP 303 状态代码?

sh 在Mac 10.11.5及更高版本的10.11系列和10.12.1及更高版本的10.12系列中安装Caffe

AVKit.AVPlayerViewController - 控件在 iOS11 及更高版本中不可见

从 iphone SDK 3.3 及更高版本中的 plist 文件创建数组:目标 c

在 SQL Plus 中,当在 Oracle 11g 及更高版本中使用 lpad 时,第二列会获得额外的尾随空格