【发布时间】:2013-09-02 11:30:21
【问题描述】:
(注意:对于Massive CPU load using std::lock (c++11) 的评论,其中大部分内容是多余的,但我认为这个话题值得拥有自己的问题和答案。)
我最近遇到了一些看起来像这样的示例 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 要求: 每个模板参数类型都应满足 Lockable 要求,[注:
unique_lock类模板满足这些 适当实例化时的要求。 ——尾注]5 效果:所有参数都通过对
lock()的一系列调用锁定,try_lock()或unlock()在每个参数上。调用顺序应 不会导致死锁,但未指定。 [注:A 必须使用 try-and-back-off 等死锁避免算法,但 未指定特定算法以避免过度约束 实施。 — 结束注释] 如果调用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 接口对工程学毫无用处。我很想被证明是错误的。
【问题讨论】:
-
对其进行编码并尝试使其处于实时锁定状态。
-
@HowardHinnant:这证明什么都没有。你可以运行一个程序一万亿次并且永远不会触发竞争条件;这并不能证明没有竞争条件。简单想象一下,两个线程同时运行示例 1,逐行遍历您的代码in lockstep;即两个线程始终在同一行代码上。 (当然,在现实生活中,调度抖动最终可能会破坏活锁。这并不意味着您的 算法 不会受到活锁的影响。它显然会...)
-
活锁状态具有非常正的特征值。如果真的形成了活锁状态,它会很快分崩离析。这不会是一个稳定的状态。
-
-1 因为你的 cmets。您似乎只想咆哮而不是解决问题。
-
@Nemo,我仍然不确定你是否理解序列是什么。
30.4.3明确指出,对于对std::lock的单个 调用:所有参数都通过对每个参数的lock()、try_lock() 或unlock() 的一系列调用来锁定.调用顺序不应导致死锁。多次调用std::lock不会避免死锁,因为这超出了本节讨论的顺序。
标签: c++ multithreading c++11 language-lawyer