数据共享之死锁

来源:互联网 发布:js 二进制流转字符串 编辑:程序博客网 时间:2024/05/21 17:04

在使用互斥量时可能会造成死锁。造成死锁,必须满足四个条件:

1、互斥使用(资源独占) 
 一个资源每次只能给一个进程使用 
2、不可强占(不可剥夺) 
    资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放 
3、请求和保持(部分分配,占有申请) 
一个进程在申请新的资源的同时保持对原有资源的占有(只有这样才是动态申请,动态分配) 
4、循环等待 
存在一个进程等待队列 
    {P1 , P2 , … , Pn}, 
    其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路

在使用互斥量时,有2中情况容易发生死锁。

1、两个(或多个线程)各自持有一个互斥量,且还需要一个(或多个)互斥量,而这个互斥量被对方已经上锁。

一个最简单情况:线程A和线程B,都需要两个锁,但是每个线程却持有一个。

还有一种情况就是加锁顺序不一致。例如要加两个锁m1和m2,一个函数先对m1加锁再对m2加锁,另一个函数相反,这就容易发生死锁。

一个例子:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest1.cpp

#include<thread>#include<mutex>#include<unistd.h>class Test{private:std::mutex m1;std::mutex m2;public:void fun1(){std::lock_guard<std::mutex> guard1(m1);//休眠,使死锁更容易发生sleep(1);std::lock_guard<std::mutex> guard2(m2);}void fun2(){std::lock_guard<std::mutex> guard1(m2);//休眠,使死锁更容易发生sleep(1);std::lock_guard<std::mutex> guard2(m1);}};void fun1(Test *p){p->fun1();}void fun2(Test *p){p->fun2();}int main(){Test t;std::thread A(fun1, &t);std::thread B(fun2, &t);A.join();B.join();return 0;}


 

2、对同一个互斥量两个加锁(互斥量是非递归)

一个例子:https://github.com/KangRoger/CppConcurrencyInAction/blob/master/DeadLocktest2.cpp

#include<thread>#include<mutex>#include<iostream>class Test{private:std::mutex m1;public:void fun1(){std::lock_guard<std::mutex> guard1(m1);fun2();}void fun2(){std::lock_guard<std::mutex> guard1(m1);}};void fun(Test *p){p->fun1();std::cout << "fun1" << std::endl;}int main(){Test t;std::thread A(fun, &t);A.join();return 0;}


 

在使用两个(以上)互斥量时,确保加锁顺序相同就不会出现死锁。

有时候确定加锁顺序也会导致死锁。例如,两个锁来保护一个类的两个实例,一个函数实现交换这两个实例(函数第一个参数先加锁,第二个参数后加锁),当交换的两个实例是同一个实例时也会发生死锁。

在C++标准库中,有方法来解决这个问题:使用std::lock函数,它能一次性格两个以上互斥量上锁,并且确保不会发生死锁。

#include<mutex>class some_big_object{};void swap(some_big_object& lhs, some_big_object& ths);class X{private:some_big_object some_detail;std::mutex m;public:X(some_big_object const& sd) :some_detail(sd){}friend void swap(X& lhs, X& rhs){if (&lhs == rhs)return;std::lock(lhs.m, rhs.m);//同时给两个互斥量上锁//下面只是转移互斥量控制权,并没有给互斥量上锁//为了确保超出作用域是,lock_guard给互斥量解锁std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);swap(lhs.some_detail, rhs.some_detail);}};

当std::lock给多个互斥量加锁时,如果其中一个加锁失败,那么会抛出异常,并且已经加锁的互斥量也会自动释放锁。

在《Linux多线程服务器编程》中看到过一个方法:给多个互斥量加锁时,按照互斥量地址高低来加锁,这样就确保了加锁顺序。

避免死锁的方法

死锁不仅仅是在使用锁的时候发生(尽管这是最常见的),创建两个线程,在每个线程中调用对方的join函数,这时候也会造成死锁。两个线程都在等待对方先结束。避免死锁有一个最简单的原则:如果这个线程可能等待你,那么你就不要等待这个线程。
1、避免给一个锁嵌套上锁
在持有一个锁的时候,不要再给这个锁上锁。如果使用多个锁,使用std::lock。
2、在持有锁时,不要调用别人提供的函数
因为你不清楚别人的代码怎么实现的,不知道它是不是在使用锁。
3、给多个锁上锁时,固定顺序。
如果在给多个所上锁,并且无法使用std::lock,最好的做法就是在每一个线程中,都按照同样的顺序。
4、分层次来使用锁
把程序分成几个层次。区分每个层次中使用的锁,当一个线程已经持有更低层次的锁时,不允许使用高层次的锁。可以在程序运行时给不同的锁加上层次号,记录每个线程持有的锁。
#include <stdexcept>#include<thread>#include<mutex>class hierarchical_mutex//实现mutex的三个接口lock,unlock,trylock{std::mutex internal_mutex;unsigned long const hierarchy_value;//记录mutex所在层次unsigned long previous_hierarchy_value;//记录上一个mutex的层次,解锁时恢复线程的层次//thread_local每一个线程都有自己的该全局变量的实例(instance)static thread_local unsigned long this_thread_hierarchy_value;//线程所在层次,是thread_localvoid check_for_hierarchy_violation(){//线程所在层次要大于当前的mutex的层次,否则抛出异常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) :hierarchical_mutex(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 unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULLONG_MAX);


使用std::unique_lock灵活上锁

std::unique_lock比std::lock_guard更加灵活,它可以先占有在一个互斥量的所有权,再给它上锁。lock_guard在占有控制权的同时就上锁了。
class X{private:some_big_object some_detail;std::mutex m;public:X(some_big_object const& sd) :some_detail(sd){}friend void swap(X& lhs, X& rhs){if (&rhs == &lhs)return;//下面是先占有锁,参数std::defer_lock表明先占有锁std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);std::unique_lock<std::mutex> lock_a(rhs.m, std::defer_lock);std::lock(lock_a, lock_b);swap(lhs.some_detail, rhs.some_detail);}};

 转移锁的控制权

std::unique_lock的实例没有占有锁的控制权,通过移动实例可以实例之间传递锁的控制权。有些情况下,移动是原子的,例如std::move()。std::unique_lock
可以转移(moveable),但是不可以拷贝。

一种用法是在一个函数给一个互斥量上锁,然后把锁的控制权交给另一个函数。
std::unique_lock<std::mutex> get_lock()//上锁,然后转移锁的控制权{extern std::mutex some_mutex;//引用外部互斥量std::unique_lock<std::mutex> lk(some_mutex);prepare_data();return lk;}void process_data(){std::unique_lock<std::mutex> lk(get_lock());do_something();}

以适当的粒度加锁

在需要使用锁的地方使用锁,尽量减小锁的有效区域。在涉及到I/O操作的函数中,尽量不要使用锁。因为I/O操作很耗时。std::unique_lock()可以在不使用共享数据时解锁,在需要使用时再上锁。
void get_and_process_data(){std::unique_lock<std::mutex> my_lock(the_mutex);//上锁some_class data_to_process=get_next_data_chunk();my_lock.unlock();//解锁result_type result=process(data_to_process);my_lock.lock();write_result(data_to_process,result);}

在使用锁保护的数据时,如果数据量非常小,拷贝它的时间比上锁时间还小,那可以考虑拷贝。例如比较两个int类型数据,拷贝它们比用锁保护它们时间更短。
class Y{private:int some_detail;mutable std::mutex m;int get_detail() const{std::lock_guard<std::mutex> lock_a(m);return some_detail;}public:Y(int sd):some_detail(sd){}friend bool operator==(Y const& lhs, Y const& rhs){if(&lhs==&rhs)return true;int const lhs_value=lhs.get_detail();int const rhs_value=rhs.get_detail();return lhs_value==rhs_value;}};

0 0