5. 使用互斥量保护共享数据

使用互斥量保护共享数据

  1. C++中使用互斥量
    • C++通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行加锁,unlock()进行解锁,在实践中不推荐直接调用成员函数,因为调用成员函数就意味着,必须记住在每个函数的出口都需要调用unlock(),同时包括异常的情况,C++中推荐使用lock_guard实例对数据进行保护 ,lock_guardmutex都在<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();
    }
    

    上述代码中使用全局变量进行保护,通常情况下没有问题,但是大多情况下,互斥量和保护的数据放在同一个类中。
    当一个成员函数返回的是保护数据的指针的或者引用的时候,会破坏数据的保护,具有访问能力的指针或者引用可以访问或者修改被保护的数据,而不会被互斥锁限制,因此在设计互斥量的时候需要能够锁住任何数据的访问方式,不留后门


  1. 精心组织代码来保护共享数据
    • 在确保成员函数不会传出指针或者引用的同时,检查成员函数是否通过指针或者引用的方式了来调用同样重要,函数可能在没有互斥量保护的地方存储这指针或者引用,例如如下代码:
    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);}

    上述代码中,虽然有互斥锁的保护,但是foo中的数据依旧通过指针被传递给了danger_pointer,切记不要将受保护的数据的指针或者 引用传递到互斥锁的作用和与之外


  1. 发现接口内在的条件竞争
    • 元素操作
      在多线程开发中,对于共享资源存在竞争条件,例如上一个线程可能对栈进行了某种判别条件,但是时间片很快切给了下一个线程,可能会导致上一个进程的判别条件发生改变,但是程序不会重复执行判别代码,如下;
    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();}

    在上述例子中,可能第一个进程刚 判断了栈为空,还没进行下面的操作,第二个线程就向栈中添加了元素,第一个线程的判断条件将不在成立,但是仍然会进行下面的操作。


  1. 多线程中的元素转移问题
    举个例子,假设有一个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. 死锁问题
    死锁产生的四个必要条件分别为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

死锁的避免方法如下:

  • 避免嵌套所
  • 避免在持有锁的时候调用用户提供的代码
  • 按照一定的顺序上锁
  • 使用锁的层次结构
    对于这个方法,他的意思是对每个锁进行级别标注,当低层次的锁被锁上的时候,高层次的锁不允许上锁,原理类似于按一定的顺序上锁

原文链接: https://www.cnblogs.com/hhyandcpp/p/17023421.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    5. 使用互斥量保护共享数据

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/311082

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 上午11:51
下一篇 2023年2月16日 上午11:52

相关推荐