死锁大多由lock引起,但是有时候没有执行lock操作仍然可以引起死锁。例如,两个线程互相jion导致的死锁;或者多个线程形成的一个循环join导致的死锁。避免死锁的方法可以归结为一句话:不要等待另一个线程,如果它此时正在等待你。下面是作者提供的如果识别和消除死锁的几个准则:
一、避免嵌套锁
首先的建议是,不要重复锁定。如果你已经锁定了就不要再次锁定。如果想同时对多个对象进行锁定操作,那么使用std::lock()函数。
二、当你已经保持一个锁状态时,避免调用用户提供的代码
很多时候你无法判断用户提供的代码究竟做了什么,他有可能做任何事,包括请求一个锁操作。如果你已经获取并保持了一个锁状态,此时你调用的用户代码如果再去请求一个锁定操作,那么久违反了第一条准则。如果你不能避免调用用户代码,那么你需要一个新的准则。
三、按固定顺序获取锁
如果你不得不请求多个锁,而且你无法使用std::lock()函数,那么此时你需要做的就是在每个线程中按固定的顺序去请求这些锁。但是有时这种方法也会出问题,例如3.1节中说的双向链表,一种可能的保护策略是为每个节点配置一个mutex,如果要删除一个节点,你必须锁定包括要删除的节点以及其前一个以及后一个节点在内的3个节点。如果想要遍历这个list,那么你需要在锁定当前节点的同时获取下一个节点,这样做的目的是为了保证下一个节点没有被修改,当获取到下一个节点后,之前的节点就可以解锁了。这种策略正常工作的前提是,不同的线程必须按同样的顺序锁定节点。如果两个线程按相反的方向遍历list,那么就会产生死锁。或者,如果一个线程试图删除一个节点,另一线程试图遍历节点,仍然会产生死锁。
一种解决方案就是规定一个遍历的顺序以避免死锁,当然这也付出了相应的代价:不允许反向遍历。类似的情况也会发生在其他数据结构中。
四、使用自定义的层级锁
一个层级锁规定只能按由高到低的顺序执行锁操作。可以为每个层级分配一个序号,并可在运行时检查和记录不同层级锁之间的操作顺序。下面列出一个层级锁的使用程序:
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
int do_low_level_stuff();
int low_level_func()
{
std::lock_guard<hierarchical_mutex> lk(low_level_mutex);
return do_low_level_stuff();
}
void high_level_stuff(int some_param);
void high_level_func()
{
std::lock_guard<hierarchical_mutex> lk(high_level_mutex);
high_level_stuff(low_level_func());
}
void thread_a()
{
high_level_func();
}
hierarchical_mutex other_mutex(100);
void do_other_stuff();
void other_stuff()
{
high_level_func();
do_other_stuff();
}
void thread_b()
{
std::lock_guard<hierarchical_mutex> lk(other_mutex);
other_stuff();//将记录错误并引发异常
}
thread_a()函数按照正确的顺序执行锁操作,没有问题。但是thread_b()函数中的操作违背了原则,因此将导致异常。使用层级锁不太可能会导致死锁,因为在任意时刻你不能保持两个同级的锁,因此,之前所说的在链表中逆序遍历也会自动被避免。
实现一个层级锁不是很难的事情。为了让std::lock_guard<>使用,我们需要提供三个函数:lock()、unlock()、try_lock()。下面的程序给出一个简单的实现,其中没有直接使用try_lock()函数,try_lock的功能是在非阻塞的情况下试图锁定指定的mutex,如果成功返回true,失败返回false。
class hierarchical_mutex
{
std::mutex internal_mutex;
unsigned long const hierarchy_value;
unsigned long previous_hierarchy_value;
static thread_local unsigned long this_thread_hierarchy_value;
void check_for_hierarchy_violation()
{
if (this_thread_hierarchy_value <= hierarchy_value)
{
throw std::logic_error(“mutex hierarchy violated”);
}
}
void update_hierarchy_value()
{
previous_hierarchy_value = this_thread_hierarchy_value;
this_thread_hierarchy_value = hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value) :hierarchy_value(value),previous_hierarchy_value(0)
{}
void lock()
{
check_for_hierarchy_violation();
internal_mutex.lock();
update_hierarchy_value();
}
void unlock()
{
this_thread_hierarchy_value = previous_hierarchy_value;
internal_mutex.unlock();
}
bool try_lock()
{
check_for_hierarchy_violation();
if (!internal_mutex.try_lock())
return false;
update_hierarchy_value();
return true;
}
};
thread_local 用来声明一个变量为线程本地变量,只用于声明全局变量或者代码块中的static变量。每个线程中拥有一个自己的变量副本,不同线程之间的副本没有关联。
ULONG_MAX代表最大的无符号long型的最大值。
它的特点是:只在每个线程中起作用,不同线程之间没有关联。在同一个线程中,每次只能锁定比上一次等级低的mutex,只要锁定过一个等级后,那么即使重新定义一个hierarchical_mutex对象,也只能锁定比之前等级低的mutex。这当然也不允许重复锁定同一个等级的mutex,就算你释放了再重新锁定也不行。
综上所诉,死锁可能由任何触发等待的同步操作引发,应该小心谨慎。