为避免死锁,常用的建议是始终使用相同的顺序锁定两个互斥元,但也有例外情况,如两个线程尝试通过交换参数而在相同的两个实例之间交换数据,将产生死锁。

幸运的是,C++标准库中的 std::lock 可以解决这一问题——std::lock 函数可以同时锁定两个或更多的互斥元,而没有死锁的风险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BigObject;
void swap(BigObject& lhs, BigObject& rhs);

class X
{
private:
BigObject detail;
std::mutex m;
public:
X(BigObject const& sd) : detail(sd) {}

friend void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs) // 确保是不同的实例
return ;

// 使用 std::lock_guard 的写法
std::lock(lhs.m, rhs.m); // 锁定这两个互斥元
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock); // 每个lock_guard对应一个互斥元
std::lock_guard<std::mutex> lock_b(rhs.m, std;:adopt_lock);

/* // 使用 std::unique_lock 的写法
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lock_b(rhs.m, std;:defer_lock);
std::lock(lhs.m, rhs.m);
*/

swap(lhs.detail, rhs.detail);
}
}

std::lock_guard 额外提供一个参数 std::adopt_lock 给互斥元,告知 std:lock_guard 对象该互斥元已被锁定,并且它们只应沿用互斥元上已有的锁的所有权,而不是试图在构造函数中锁定互斥元。

std:unique_lock 保留互斥元为未锁定,但占用更多空间并且使用起来比 std::lock_guard 略慢。

这就确保了通常在受保护的操作可能引发异常的情况下,函数退出时正确地解锁互斥元,这也考虑到了简单返回。此外,在对 std::lock 的调用中锁定 lhs.m 抑或是 rhs.m 都可能引发异常,在这种情况下,该异常被传播出 std::lock,如果 std::lock 已经成功地在一个互斥元上获取了锁,当它试图在另一个互斥元上获取锁的时候,就会引发异常,前一个互斥元将会自动释放。std::lock 提供了关于锁定给定的互斥元的全或无的语义。


避免死锁的一些办法:

  1. 避免嵌套锁:已经持有一个锁,就别再获取锁
  2. 在持有锁时,避免调用用户提供的代码
  3. 以固定顺序获取锁
  4. 使用锁层次:在高层锁定低层互斥元;如果在较低层已经持有锁定,则不允许锁定该互斥元