std::get<C++11多线程库~线程间共享数据>(10):使用互斥量保护共享数据(4)
1 #ifndef DEADLOCK_QUESTIONDESCRIBLE_AND_SOLUTION_H 2 #define DEADLOCK_QUESTIONDESCRIBLE_AND_SOLUTION_H 3 4 /* 5 * 话题1:使用互斥量保护共享数据 6 * 7 * 接下来学习第四个小话题:死锁,死锁的问题描述,死锁的解决方案 8 * 9 * 互斥量在解决共享数据安全的同时,除了会引入条件竞争, 还可能会引发死锁。 10 * 条件竞争使得共享数据变得不安全,而死锁会导致各线程僵持,导致线程间出现互相等待的尴尬局面。 11 * 12 * 下面我们就来学习一下, 出现死锁的场景,以及如何避免死锁的发生。 13 * 14 */ 15 16 /* 17 * 死锁的问题描述: 18 * 19 * 线程内出现两个或两个以上的互斥量时, 比如互斥量ma和mb, 有两个线程TA和TB, 线程TA先锁住了ma,准备继续对mb上锁, 而正在此时, 20 * 线程TB锁住了mb,准备继续对ma上锁。 线程TA等待着线程TB释放互斥量mb,线程TB等待线程TA释放互斥量ma, 谁也不让谁,谁也不罢休,两个线程 21 * 僵持等待。这就是死锁发生的情况。 22 */ 23 24 25 /* 26 * 死锁的解决方案: 27 * 28 * 一般的建议:让多个互斥量总是以相同的顺序上锁。互斥量mb总是在ma上锁之后上锁。可以将这个建议套入上面对死锁问题的描述,你就能够领悟了。 29 * 30 * 一般的建议往往只能搞定一般的问题, 如果多个互斥量所属于同一个作用域内,那么这个一般的建议是可以搞定的。那么多个互斥量时,如何能保证 31 * 不出问题,一定不会出现死锁呢? 32 * 33 * C++ 标准库提供了 std::lock, 可以一次性的锁住多个(两个及两个以上)互斥量,并且没有副作用(没有死锁风险)。std::lock的原则是, 34 * 要么对所有互斥量都成功上锁,要么一个互斥量也不上锁。 35 */ 36 37 #include <mutex> 38 struct Data{ 39 40 }; 41 void swap(Data & ldata, Data &rdata); 42 bool greater(Data &ldata, Data & rdata); 43 44 class DeadLock_QuestionDescrible_and_Solution; 45 void swap(DeadLock_QuestionDescrible_and_Solution &lobj, DeadLock_QuestionDescrible_and_Solution & robj); 46 bool greater(DeadLock_QuestionDescrible_and_Solution &lobj, DeadLock_QuestionDescrible_and_Solution & robj); 47 48 class DeadLock_QuestionDescrible_and_Solution 49 { 50 Data m_data; 51 std::mutex m_mutex; 52 public: 53 DeadLock_QuestionDescrible_and_Solution(Data data):m_data(data){ 54 55 } 56 57 friend void swap(DeadLock_QuestionDescrible_and_Solution &lobj, DeadLock_QuestionDescrible_and_Solution & robj) 58 { 59 if (&lobj == &robj){ 60 return ; 61 } 62 else{ 63 std::lock(lobj.m_mutex, robj.m_mutex); 64 std::lock_guard<std::mutex> l_guard(lobj.m_mutex, std::adopt_lock); 65 std::lock_guard<std::mutex> r_guard(robj.m_mutex, std::adopt_lock); 66 swap(lobj.m_data, robj.m_data); 67 } 68 } 69 70 friend bool greater(DeadLock_QuestionDescrible_and_Solution &lobj, DeadLock_QuestionDescrible_and_Solution & robj) 71 { 72 if (&lobj == &robj){ 73 return false; 74 } 75 else{ 76 std::lock(lobj.m_mutex, robj.m_mutex); 77 std::lock_guard<std::mutex> l_guard(lobj.m_mutex, std::adopt_lock); 78 std::lock_guard<std::mutex> r_guard(robj.m_mutex, std::adopt_lock); 79 return greater(lobj.m_data, robj.m_data); 80 } 81 } 82 }; 83 84 /* 85 * 上边这个例子很好的阐明了, "一般的建议只能搞定一般的问题"。 86 * 上边例子中 DeadLock_QuestionDescrible_and_Solution::swap 和 DeadLock_QuestionDescrible_and_Solution::greater 两个函数 87 * 很好的说明了问题。 88 * 89 * 我们来分析一下, 每一个 Data 实例都有一个互斥量保护, 在 swap 和 greater 函数中,就都出现了 两个实例和两个互斥量。 90 * 91 * 咋一看,如果调用 swap 和 greater 函数时,参数按相同的顺序传递,似乎也不会出现死锁, 但实际上,我们把接口提供给用户,用户是很难保证 92 * 多个参数按相同顺序调用我们提供的接口。 93 * 94 * 比如, 线程TA 调用 swap() 函数,按序传递 objA objB;线程TB 调用 greater()函数,按序传递 ojbB objA。 死锁就诞生了! 95 * 96 * 对于,一般建议提到的做法,按序对互斥量上锁, 下边给出一个例子。 97 * 98 */ 99 100 struct OtherData{ 101 102 }; 103 104 class OtherObject_ThreadSafe{ 105 Data m_data; 106 std::mutex m_data_mutex; 107 108 OtherData m_otherData; 109 std::mutex m_otherData_mutex; 110 111 void func1(){ 112 std::lock_guard<std::mutex> guard_data(m_data_mutex); 113 std::lock_guard<std::mutex> guard_otherData(m_otherData_mutex); 114 115 //m_data and m_otherData do something... 116 } 117 118 void func2(){ 119 std::lock_guard<std::mutex> guard_data(m_data_mutex); 120 std::lock_guard<std::mutex> guard_otherData(m_otherData_mutex); 121 122 //m_data and m_otherData do something... 123 } 124 }; 125 126 /* 127 * 上边例子演示了,同一作用域内,需要对多个互斥量上锁,可以采用一般的建议,按相同的顺序对互斥量进行上锁,就可以搞定即保护了共享数据,又不会出现死锁。 128 * 129 * 130 * 友情提醒: 实际编程中,能要底层级别手段搞定的问题,就尽量不要用高级的东西,毕竟越高级代价也就越高!!! 131 */ 132 133 134 /* 135 * 拓展: 136 * C++17 对组合使用 std::lock()函数 和 std::lock_guard 模板类 提供了另一个支持,那便是: std::scoped_lock模板类 137 * 138 * 因此, 上边的 swap() 函数可以改写成如下形式。 139 */ 140 141 friend void swap(DeadLock_QuestionDescrible_and_Solution &lobj, DeadLock_QuestionDescrible_and_Solution & robj) 142 { 143 if (&lobj == &robj){ 144 return ; 145 } 146 else{ 147 std::scoped_lock sl(lobj.m_mutex, robj.m_mutex); 148 swap(lobj.m_data, robj.m_data); 149 } 150 } 151 152 /* 153 * std::scoped_lock<>一种新的RAII类型模板类型,与std::lock_guard<>的功能等价, 154 * 这个新类型能接受不定数量的互斥量类型作为模板参数,以及相应的互斥量(数量和类型)作为构造参数。 155 * 互斥量支持构造即上锁,与std::lock的用法相同,其解锁阶段是在析构中进行。 156 */ 157 #endif // DEADLOCK_QUESTIONDESCRIBLE_AND_SOLUTION_H