使用互斥量保护共享数据
- C++中使用互斥量
- C++通过实例化
std::mutex
创建互斥量,通过调用成员函数lock()
进行加锁,unlock()
进行解锁,在实践中不推荐直接调用成员函数,因为调用成员函数就意味着,必须记住在每个函数的出口都需要调用unlock()
,同时包括异常的情况,C++中推荐使用lock_guard
实例对数据进行保护 ,lock_guard
和mutex
都在<mutex>
头文件中进行声明
上述代码中使用全局变量进行保护,通常情况下没有问题,但是大多情况下,互斥量和保护的数据放在同一个类中。#include<list> #include<mutex> std::list<int>li; std::mutex li_mutex; void add_to_list(int num){ std::lock_guard<mutex>guard(li_mutex); li.push_back(num); } bool list_contain(int value_to_find){ std::lock_guard<mutex>guard(li_mutex); return std::find(li.begin(),li.end(),value_to_find) != li.end(); }
当一个成员函数返回的是保护数据的指针的或者引用的时候,会破坏数据的保护,具有访问能力的指针或者引用可以访问或者修改被保护的数据,而不会被互斥锁限制,因此在设计互斥量的时候需要能够锁住任何数据的访问方式,不留后门 - C++通过实例化
- 精心组织代码来保护共享数据
- 在确保成员函数不会传出指针或者引用的同时,检查成员函数是否通过指针或者引用的方式了来调用同样重要,函数可能在没有互斥量保护的地方存储这指针或者引用,例如如下代码:
上述代码中,虽然有互斥锁的保护,但是foo中的数据依旧通过指针被传递给了danger_pointer,切记不要将受保护的数据的指针或者 引用传递到互斥锁的作用和与之外class foo{ private: int data; mutex _mutex; public: template<class Function> void dowork(Function func){ lock_guard<mutex>g(_mutex); func(data); } } int * danger_pointer; void danger_func(int & tmp){ danger_pointer = &tmp; } int main(){ foo f; f.dowork(danger_func); }
- 发现接口内在的条件竞争
- 元素操作
在多线程开发中,对于共享资源存在竞争条件,例如上一个线程可能对栈进行了某种判别条件,但是时间片很快切给了下一个线程,可能会导致上一个进程的判别条件发生改变,但是程序不会重复执行判别代码,如下;
在上述例子中,可能第一个进程刚 判断了栈为空,还没进行下面的操作,第二个线程就向栈中添加了元素,第一个线程的判断条件将不在成立,但是仍然会进行下面的操作。stack<int>sta; void process_1(){ if(sta.empty()){ ... } } void process_2(){ sta.push(...); } int main(){ thread t1(process_1); thread t2(process_2); t1.join(); t2.join(); }
- 元素操作
- 多线程中的元素转移问题
举个例子,假设有一个stack<vector>,当vector中有大量元素的时候,拷贝其中的内容可能会发生bad_alloc的异常,如果此时调用stack中的pop方法,可能造成栈顶元素确实从栈中弹出,但是并没有足够的空间对其进行拷贝,这个时候,有就会丢失,基本的解决办法就是先拷贝,再弹出,这样又会产生空间不足的问题。针对这种问题,有如下几个方法可以进行解决。 - 传入一个引用
将变量的引用作为参数传递到pop函数当中
std::vector<int>res; sta.pop(res); 此处pop函数具体如下: void pop(vector<int>& res){ lock_guard<mutex>loc(m); if(sta.empty()) throw empty_stack(); res = data.top(); data.pop(); }
- 返回弹出值的指针
返回指针和返回引用的原理是一样的
auto ptr = sta.pop(); 此处pop函数的具体实现如下: std::shared_ptr<T> pop(){ std::loc_guard<mutex>loc(m); if(data.empty()) throw empty_stack(); shared_ptr<T>const res(std::make_shared<T>(data.top())); return res; }
- 传入一个引用
- 死锁问题
死锁产生的四个必要条件分别为1. 互斥条件 2. 不可剥夺条件 3. 请求和保持条件 4. 循环等待条件
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,
而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
————————————————
版权声明:本文为CSDN博主「Hyacinth_Dy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jyy305/article/details/70077042
死锁的避免方法如下:
- 避免嵌套所
- 避免在持有锁的时候调用用户提供的代码
- 按照一定的顺序上锁
- 使用锁的层次结构
对于这个方法,他的意思是对每个锁进行级别标注,当低层次的锁被锁上的时候,高层次的锁不允许上锁,原理类似于按一定的顺序上锁