c++多线程-线程中的死锁问题
假设有一个玩具,有两部分组成。一部分是鼓另一部分是鼓锤,任何人他们想玩这个玩具的话必须要拥有这个玩具的两部分(鼓和鼓锤)。
现在假设你有两个孩子都喜欢玩这个玩具,如果其中一个孩子同时拿到鼓和鼓锤他可以快乐的玩耍,直到他玩累了不玩了。如果另一个孩子想要玩这个玩具必须等前一个孩子玩完才可以玩,尽管他不高兴。由于鼓和鼓锤是分开装在两个玩具盒,此时你的两个孩子同时想要玩这个玩具,两个孩子翻找玩具拿,一个人找到了鼓另一个找到了鼓锤,现在他们两个卡住都玩不了,除非一个很高兴的把玩具的一部分让给另外一个,让另一个玩。但是孩子都比较调皮,不懂得了谦让。每个人都坚持自己要玩,最后的问题是他们一直疆持,谁也玩不了。
想象一个,你没有孩子争玩具,而是两个线程争互斥锁:每一个线程需要去锁定一对互斥锁(mutex)去执行一个任务,一个线程已经获取了一个互斥锁(mutex),另一个也获取了一个互斥锁(mutex),每一个线程都在等待锁定另一个互斥锁(mutex),这样造成的结果是 任何一个线程都无法得到另一个互斥锁(mutex)。这种就叫死锁(deadlock),这里最大的问题是完成任务必须锁定两个或多个互斥锁(mutex)来执行操作。
这种情况通常的建议是按相同的顺序来锁定多个互斥锁(mutex),如果你一直先锁定mutex A再锁定mutex B,那一定不会发生死锁。有时这很简单,因为互斥对象有不同的用途,但有时却不那么简单,例如互斥对象分别保护同一个类的单独对象时。例如有个操作是交换同一个类的两个对象。为了保证他们两个个都交换成功,避免被多线程修改。两个对象的mutex都要锁定。如果一固定的顺序去锁定(例如先锁定第一个参数对象,再锁定第二个参数对象)这会适得其反。两个线程同时尝试交换同两个对象时,就会发生死锁。
幸好C++标准库有一个解决方法就是用 std::lock 一次去锁定多个mutex,这样不会让程序死锁。如图示例如示:
1 class some_big_object; 2 void swap(some_big_object& lhs,some_big_object& rhs); 3 class X 4 { 5 private: 6 some_big_object some_detail; 7 std::mutex m; 8 public: 9 X(some_big_object const& sd):some_detail(sd){} 10 friend void swap(X& lhs, X& rhs) 11 { 12 if(&lhs==&rhs) 13 return; 14 std::lock(lhs.m,rhs.m); 15 std::lock_guard lock_a(lhs.m,std::adopt_lock); 16 std::lock_guard lock_b(rhs.m,std::adopt_lock); 17 swap(lhs.some_detail,rhs.some_detail); 18 } 19 };
上面代码中 std::adopt_lock 代表该mutex已经lock了。这样确保这些mutex被正确的锁定(受保护的操作可能会引发异常)在函数退出时正常解锁。另外值得注意的是 在对std::lock的调用中锁定两个mutex时有可能会抛出异常,在这种情况下异常将会被std::lock传递下去。如果std::lock已经成功锁定了一个,在锁定第二个的时候抛出异常了。那么第一个lock也会正常解锁。std::lock确保要全部锁定和解除锁定。
虽然std::lock可以帮助你解决多个mutex同时需要锁定的情形,但他没法去帮助你分开锁定两个mutex。这种情况你要根据开发人员制定的规则和代码流程来确定不会发生死锁。但是这不那么简单:线程死锁是一个比较棘手的问题,而且在多线程代码中经常会碰到。有时候在开发测试的时候没有问题,但是在运行时会出现死锁。但是,有一些相对简单的规则可以帮助你编写无死锁的代码。
避免死锁的一般准则:
线程死锁只发生在有锁的情况下。有时你创建了两个个线程,这两个线程分别join另一个线程,这样任何join都无法返回,每个线程都在等待另一个线程结束,这就是两个孩子为了玩具打架一样。这种的问题可以发生在任何“一个线程正在等另一个线程做事,而另一个线程有时候也会等第一个线程做什么事”的时候。避免线程死锁归结为一个重要概念就是:A线程不要等待B线程,如果B线程有可能等待A线程。
1.避免嵌套锁定
这一条是最简单的,你已经锁定了一个mutex的时候,你最好不要再次锁定。如果你遵守了这条规则,因为一个线程只有一个锁的情况下不会造成死锁。但是也有其它原因会造成死锁(比如一个线程在等待另一个线程),如果你要锁定多个,你就用std::lock。
2.在已经持有锁的时候不要调用用户自义的代码
因为用户自定义的代码是无法预知的,谁知道他的代码里会不会也想要锁定这个lock。有时候无法避免不调用用户定义代码,这种情况下,你需要注意。
3.按固定顺序锁定
如果你要锁定两个以上的mutex而你又不能用std::lock。那么最好的建议就按固定顺序去锁定。
4.用层锁来防止死锁
hierarchical_mutex规则思想是:将mutex分层,规定加锁顺序是由高层到底层才能进行,底层到高层报出运行时错误,这样就可以利用编程的方法检测死锁。书中实现了hierarchical_mutex类作为可分层的mutex,先列出使用方法如下
1 hierarchical_mutex high_level_mutex(10000); 2 hierarchical_mutex low_level_mutex(500); 3 void ThreadA(){ 4 std::lock_guard<hierarchical_mutex> lock1(high_level_mutex); 5 ... //做一些使用high_level_mutex就可以干的事 6 std::lock_guard<hierarchical_mutex> lock1(low_level_mutex); 7 ... //需要两个mutex同时加锁才可以干的事 8 9 } 10 11 void ThreadB(){ 12 std::lock_guard<hierarchical_mutex> lock1(low_level_mutex); 13 ... //做一些使用low_level_mutex就可以干的事 14 //对高低层mutex加锁的情况下,对高层mutex加锁,不符合规定的顺序,抛出异常! 15 std::lock_guard<hierarchical_mutex> lock1(high_level_mutex); 16 }
5.这些准则超越了束缚
正如我在本节开头提到的那样,死锁不仅发生在锁任何可能导致等待周期的同步结构都可能发生这种情况。它的因此,值得将这些准则扩展到涵盖这些情况。例如,只是因为您应该尽可能避免获取嵌套锁,所以等待一个线程同时持有锁,因为该线程可能需要获取锁为了继续。同样,如果您要等待线程完成,则可能是值得标识线程层次结构,以便线程仅等待较低的线程在层次结构中。一种简单的方法是确保您的线程设计完避免死锁的代码后,请执行std :: lock()和std ::lock_guard涵盖了大多数简单锁定的情况,但有时更具灵活性是必须的。对于这些情况,标准库提供了std :: unique_lock模板。像std :: lock_guard一样,这是在互斥锁上参数化的类模板类型,并且还提供与std :: lock_guard相同的RAII样式的锁管理但灵活性更高。