zoukankan      html  css  js  c++  java
  • 数据共享之死锁

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

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

    在使用相互排斥量时。有2中情况easy发生死锁。

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

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

    另一种情况就是加锁顺序不一致。

    比如要加两个锁m1和m2。一个函数先对m1加锁再对m2加锁,另一个函数相反,这就easy发生死锁。

    一个样例: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);
    		//休眠,使死锁更easy发生
    		sleep(1);
    		std::lock_guard<std::mutex> guard2(m2);
    	}
    	void fun2()
    	{
    		std::lock_guard<std::mutex> guard1(m2);
    		//休眠,使死锁更easy发生
    		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多线程server编程》中看到过一个方法:给多个相互排斥量加锁时。依照相互排斥量地址高低来加锁,这样就确保了加锁顺序。

    避免死锁的方法

    死锁不不过在使用锁的时候发生(虽然这是最常见的),创建两个线程。在每一个线程中调用对方的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_local
    
    	void 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;
    		}
    };

  • 相关阅读:
    入门系列4
    入门系列3
    入门系列2
    入门系列1
    sql进阶-筛选库表中数据为空的表
    sql进阶-删除所有的视图
    sql序列(2) sql语句功能表
    sql序列(5)事务
    sql序列(4)存储过程
    KVM虚拟化介绍
  • 原文地址:https://www.cnblogs.com/lxjshuju/p/6866975.html
Copyright © 2011-2022 走看看