赞
踩
例子:
class some_big_obj { // ... }; void swap(some_big_obj& lhs, some_big_obj& rhs); class X { public: X(some_big_obj const& sd):some_detail(sd){} friend void swap(X& lhs, X& rhs) { if (&lhs == &rhs) return; std::lock_guard laoc_a(lhs.m); // 1 std::lock_guard laoc_b(rhs.m); // 2 swap(lhs.some_detail, rhs.some_detail); } private: some_big_obj some_detail; std::mutex m; };
上面的1处和2处分别锁住了两个互斥量,假如线程A在执行1的时候,线程切换d到B,B线程锁住了rhs的锁,并开始尝试锁住lhs的锁,但是发现lhs的锁已经被锁住了(被A锁住了),所以B线程发生了阻塞。然后线程切换回A,开始去尝试锁住rhs,但是rhs此时已经被B线程锁住了,此时A线程也发生了阻塞。两个线程会分别等待对方释放另一个锁,所以就无限等待了。
解决方法:
class some_big_obj { // ... }; void swap(some_big_obj& lhs, some_big_obj& rhs); class X { public: X(some_big_obj const& sd):some_detail(sd){} friend void swap(X& lhs, X& rhs) { if (&lhs == &rhs) return; std::scoped_lock lock(lhs.m, rhs.m); // 1 C++17 // 2 C++11 // std::lock(lhs.m, rhs.m); // std::lock_guard lock_a(lhs.m, std::adopt_lock); // std::lock_guard lock_b(rhs.m, std::adopt_lock); swap(lhs.some_detail, rhs.some_detail); } private: some_big_obj some_detail; std::mutex m; };
有两种方法:
std::scope_lock
模板类,此模板类可以同时锁定多个互斥量,并且能够在析构时自动解锁互斥量;std::lock()
模板函数和std::lock_guard
模板类,通过函数std::lock()
锁定多个互斥量,然后通过std::lock_guard
类负责互斥量的解锁。其中,std::adopt_lock
表示只构造对象,但不锁定互斥量。例子:
std::mutex m; void f() { // .... std::lock_guard lock(m); // 1 子线程锁住互斥量m // ... } int main() { std::thread t(f); std::lock_guard lock(m); // 2 主线程锁住互斥量m // ... t.join(); // 3 等待子线程结束 return 0; }
上述过程可能导致在2处上锁,然后子线程在1处发生阻塞,最后主线程在3处一直等待子线程结束,无穷等待下去。
解决方法:
join()
之后; std::thread t(f);
// ...
t.join();
// ...
std::lock_guard lock(m);
// ...
std::thread t(f);
{
std::lock_guard lock(m);
// ...
}
t.join();
因为用户提供的代码中很可能也有锁,这样的话,在同一个过程可能就会访问多个锁,可能会产生第1点所描述的死锁。
比如多线程下对于链表的操作。假如保护链表的锁的粒度很小,每个节点拥有一个互斥量,这样能够最大化并行效率。在这种情况下,当一个线程删除链表中的一个节点时,它必须获取3个节点上的互斥量:将要删除的节点,两个邻接节点。
线程1 | 线程2 |
---|---|
锁住主入口的互斥量 | |
读取头节点指针 | |
锁住头节点互斥量 | |
解锁主入口互斥量 | |
锁住主入口互斥量 | |
读取head->next指针 | 锁住尾结点互斥量 |
锁住next节点互斥量 | 读取tail->prev指针 |
读取next->next指针 | 解锁尾结点互斥量 |
… | … |
锁住A节点互斥量 | 锁住C节点互斥量 |
读取A->next指针(也就是B节点) | 读取C->next指针(也就是B节点) |
锁住B节点互斥量 | |
阻塞,尝试锁住B节点互斥量 | 解锁C节点互斥量 |
读取B->prev指针(也就是A节点) | |
阻塞,尝试锁住A节点互斥量 | |
死锁 | 死锁 |
由于这里获取三个锁不是同时的,是分步获取的,所以不能够使用std::scoped_lock
,所以就会产生上述的死锁。
避免这种死锁的方式就是定义遍历的顺序,一个线程必须先锁住A才能获取B的锁,在锁住B之后才能获取C的锁。
在某些情况下,设定锁的大小,使得一个线程中获取多个锁时,只能按照顺序来进行上锁,如果顺序错了,直接抛出异常,这样就能避免死锁。
层次锁的实现例子:
class hierarchical_mutex { private: std::mutex internal_mutex; const unsigned long hierarchy_value; unsigned long previous_hierarchy_value; // thread_local是线程局存储类型 static thread_local unsigned long this_thread_hierarchy_value; private: 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() { if (this_thread_hierarchy_value != hierarchy_value) throw std::logic_error("mutex hierarchy violated"); 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 unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);
其中,thread_local
类型用于线程局部存储(STL)。需要注意的是,如果想让自定义锁类型能够使用标准库的std::lock()
或std::lock_guard
,则必须自己实现类成员函数lock()
,unlock()
和try_lock()
。
层次锁的使用例子:
hierarchical_mutex high_level_mutex(10000); hierarchical_mutex low_level_mutex(5000); hierarchical_mutex other_mutex(6000); int do_low_level_stuf(); int low_level_func() { std::lock_guard lk(low_level_mutex); return do_low_level_stuf(); } void do_high_level_stuff(int some_param); void high_level_func() { std::lock_guard lk(high_level_mutex); do_high_level_stuff(low_level_func()); } void thread_a() { high_level_func(); } void do_other_stuff(); void other_func() { high_level_func(); do_other_stuff(); } void thread_b() { std::lock_guard lk(other_mutex); other_func(); }
上述例子中,thread_a
线程先得到标记为10000的锁,然后又得到标记为5000的锁,因此能正常执行。而thread_b
先得到标记为6000的锁,然后又尝试获取标记为10000的锁,这里就会出错,因为获取锁的顺序出错了,最后会抛出异常。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。