(注意:其中大部分内容对于使用 std::lock (c++11)的大规模 CPU 负载的评论是多余的,但我认为这个主题值得拥有自己的问题和答案。)
我最近遇到了一些看起来像这样的示例 C++11 代码:
std::unique_lock<std::mutex> lock1(from_acct.mutex, std::defer_lock);
std::unique_lock<std::mutex> lock2(to_acct.mutex, std::defer_lock);
std::lock(lock1, lock2); // avoid deadlock
transfer_money(from_acct, to_acct, amount);
哇,我想,std::lock
听起来很有趣。我想知道标准说它做了什么?
C++11 第 30.4.3 节 [thread.lock.algorithm],第 (4) 和 (5) 段:
模板无效锁(L1&, L2&, L3&...);
4要求:每个模板参数类型应满足可锁定的要求,[注:
unique_lock
类模板在适当实例化时满足这些要求。——尾注]5效果:所有参数都通过对每个参数
lock()
的 一系列调用锁定。调用序列不应导致死锁,但未指定。[注:必须使用try-and-back-off等死锁避免算法,但未指定具体算法以避免过度约束实现。— 尾注] 如果调用or抛出异常,则应调用任何已被调用or锁定的参数。try_lock()
unlock()
lock()
try_lock()
unlock()
lock()
try_lock()
考虑以下示例。称之为“示例 1”:
Thread 1 Thread 2
std::lock(lock1, lock2); std::lock(lock2, lock1);
这种僵局可以吗?
对标准的简单阅读说“不”。伟大的!也许编译器可以为我订购我的锁,这会很整洁。
现在尝试示例 2:
Thread 1 Thread 2
std::lock(lock1, lock2, lock3, lock4); std::lock(lock3, lock4);
std::lock(lock1, lock2);
这种僵局可以吗?
再一次,标准的简单阅读说“不”。哦哦。做到这一点的唯一方法是使用某种退避和重试循环。更多关于下面的内容。
最后,示例 3:
Thread 1 Thread 2
std::lock(lock1,lock2); std::lock(lock3,lock4);
std::lock(lock3,lock4); std::lock(lock1,lock2);
这种僵局可以吗?
再一次,对标准的简单解读说“不”。(如果这些调用之一中的“调用序列lock()
”不是“导致死锁”,那么究竟是什么?)但是,我很确定这是无法实现的,所以我想这不是他们的意思。
这似乎是我在 C++ 标准中见过的最糟糕的事情之一。我猜它一开始是一个有趣的想法:让编译器分配一个锁顺序。但是一旦委员会咀嚼它,结果要么无法实现,要么需要重试循环。是的,这是个坏主意。
您可以争辩说“退出并重试”有时很有用。这是真的,但只有当你不知道你试图抢在前面的锁时。例如,如果第二个锁的身份取决于第一个锁保护的数据(比如因为您正在遍历某些层次结构),那么您可能需要进行一些抓取-释放-抓取旋转。但在那种情况下,你不能使用这个小工具,因为你不知道前面的所有锁。另一方面,如果您确实知道要预先使用哪些锁,那么您(几乎)总是只想简单地强加一个排序,而不是循环。
另外,请注意,如果实现只是按顺序获取锁、后退和重试,则示例 1 可以活锁。
简而言之,这个小工具在我看来充其量是没用的。只是一个坏主意。
好的,问题。(1) 我的任何主张或解释是否错误?(2) 如果不是,他们到底在想什么?(3)我们是否都同意“最佳实践”是std::lock
完全避免?
[更新]
一些答案说我误解了标准,然后继续以与我相同的方式解释它,然后将规范与实现混淆。
所以,要明确一点:
在我对标准的阅读中,示例 1 和示例 2 不能死锁。示例 3 可以,但只是因为在这种情况下避免死锁是无法实现的。
我的问题的全部要点是,避免示例 2 的死锁需要一个回退和重试循环,而这样的循环是非常糟糕的做法。(是的,对这个微不足道的示例进行某种静态分析可以避免这种情况,但在一般情况下并非如此。)还要注意 GCC 将此事物实现为繁忙循环。
[更新 2]
我认为这里的很多脱节是哲学上的基本差异。
编写软件有两种方法,尤其是多线程软件。
在一种方法中,您将一堆东西放在一起并运行它以查看它的工作情况。你永远不会相信你的代码有问题,除非有人能在一个真实的系统上证明这个问题,现在,今天。
在另一种方法中,您编写的代码可以被严格分析以证明它没有数据竞争,它的所有循环都以概率 1 终止,等等。您严格在语言规范保证的机器模型内执行此分析,而不是在任何特定实现上。
后一种方法的拥护者对特定 CPU、编译器、编译器次要版本、操作系统、运行时等的任何演示都没有留下深刻印象。这样的演示几乎没有兴趣,也完全无关紧要。如果您的算法存在数据竞争,那么无论您运行它时发生什么,它都会被破坏。如果你的算法有一个活锁,它就会被破坏,不管你运行它时会发生什么。等等。
在我的世界中,第二种方法称为“工程”。我不确定第一种方法叫什么。
据我所知,该std::lock
界面对工程学毫无用处。我很想被证明是错误的。