《C++ Concurrency in Action》笔记8 死锁(1)

来源:互联网 发布:学生化妆品推荐 知乎 编辑:程序博客网 时间:2024/06/10 08:21

就像之前关于top和pop的讨论,他的问题本质上来说是因为lock了太小的单元,保护并不能覆盖几个操作的组合。但是,另一方面,如果lock了太大的范围,同样也能导致问题。极端的情况就是一个全局mutex对象保护了所有的共享数据。在一个存在着大量共享数据的系统中,这将抵消并发带来的任何效益增值,因为它强迫在同一时刻只能运行一个线程,即使这些线程访问的是不同的共享数据。Linux内核的第一个版本就是被设计成使用一个全局内核锁。尽管他能工作,但是这意味着一个双核系统的性能明显低于两个单核系统。后来的Linux版本采用了更加细粒度的锁策略,解决了这个问题。

使用细粒度锁方案的一个问题就是,为了在一个操作中保护所有数据,你需要更多的mutex。如前所说,有时候需要增加锁粒度,以至只需要一个mutex去执行锁定操作。但有时候这是不可取的,例如多个mutex保护了一个类的多个实例。

In this case, locking at the next level up would mean either leaving the locking to the user or having a single mutex that protected all instances of that class, neither of which is particularly desirable.

在这种情况下,进入下一层锁就意味着要么将锁丢给用户去执行,要么使用一个单独的mutex去保护这个类的所有实例,但两者都不令人满意。(不知道翻译的是否准确)

如果你最终不得不在一个操作中使用多个mutex,那么还有另外一个隐藏的风险:死锁。这种情况几乎完全与race condition相反,多个线程都想争得控制权,但却都在等待对方释放控制权,结果只能无限等下去。

一种常见的避免死锁的方式是,总是以同样的顺序锁定2个mutex。但有时不太容易,例如多个mutex负责保护一个类的多个实例。想象一个例子:一个操作用于交换同一个类的两个实例,如果选择固定顺序(例如,先是锁定与第一个参数对应的mutex,然后再锁定与第二个参数对应的mutex),那将适得其反:如果两个线程同时针对相同的一对实例进行交换操作,只是参数顺序不同,那么将产生死锁。

幸运的是,C++标准库提供了一个函数来解决这种问题:std::lock(),可以一次性锁定2个或更多的mutex,而且不会导致死锁。下面的示例演示如何使用这个函数:

class some_big_object{};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);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);}};

首先检查两个参数是否不同,因为试图锁定一个已经被自己锁定的mutex是未定义行为( std::recursive_mutex可以被同一个线程多次锁定)。然后使用std::lock()函数锁定两个mutex,再构造2个std::lock_guard对象,std::adopt_lock参数用于通知std::lock_guard对象:为其提供的mutex已经被锁定,在构造时不要再次锁定mutex参数。然后进行具体的数据交换。当函数执行完毕后,2个std::lock_guard对象的析构函数负责解锁其对应的mutex对象,这一点我们通过查看std::lock_guard的源码(vs2013)可以看得十分清楚:

template<class... _Mutexes>class lock_guard{// class with destructor that unlocks mutexespublic:explicit lock_guard(_Mutexes&... _Mtxes): _MyMutexes(_Mtxes...){// construct and lock_STD lock(_Mtxes...);}lock_guard(_Mutexes&... _Mtxes, adopt_lock_t): _MyMutexes(_Mtxes...){// construct but don't lock}~lock_guard() _NOEXCEPT{// unlock all_For_each_tuple_element(_MyMutexes,[](auto& _Mutex) _NOEXCEPT { _Mutex.unlock(); });}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;private:tuple<_Mutexes&...> _MyMutexes;};

此处仅将lock_guard类模板的部分定义贴了出来,还有两个模板特化代码没有贴出来分别是只有一个mutex参数和没有参数的定义。如果没有参数则lock_guard什么也不做。

具有std::adopt_lock类型参数的构造函数,其实这个参数仅仅起到一个通知的作用,此时构造函数不去对mutex执行lock操作。注意析构函数,无论如何都执行mutex的unlock操作。通过分析std::lock_guard的源码,也会知道,std::lock()函数仅仅负责锁定其参数对应的mutex,别的什么也不做。解锁是由std::lock_guard来做的。

注意多参数模板定义的构造函数,在函数体内自动调用了std::lock()函数。因此上面的swap()函数可以简化成如下版本:

void swap1(X& lhs, X& rhs){if (&lhs == &rhs)return;lock_guard<mutex,mutex> lg(lhs.m, rhs.m);swap(lhs.some_detail, rhs.some_detail);}

还要注意的是,std::lock()函数在试图锁定一个mutex时可能引发异常,例如,当成功锁定一个mutex后在试图锁定第二个mutex时即使产生来了异常一会保证释放第一个mutex。它遵循一个原则:要么都锁定要么都不锁定。

虽然,std::lock()函数可以帮你同时锁定多个mutex,但是它无法解决这些mutex同时被其他线程分别单独锁定而带来的问题,此时只能靠一个程序员的经验了。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 二年级的孩子浮躁上课不认真怎么办 三年级的小孩上课不认真听讲怎么办 大班孩子上课不专心听讲玩怎么办 老师跟家长说小孩上课讲话怎么办 别的家长动手打自己的孩子怎么办? 一岁3个月宝宝骨龄偏小怎么办 宝宝9个月了越来越粘人怎么办? 孩子突然说话结巴口吃了怎么办啊 很久不说话了不敢说话了怎么办 宝宝2岁了还不会说话怎么办 2岁的宝宝还不会说话怎么办 小宝宝有四个月了母乳不够吃怎么办 一个月的宝宝吐奶厉害怎么办 新生儿吐奶吐一次吐的特别多怎么办 把孩子打了一次现在说话结巴怎么办 宝宝五岁了口吃越来越严重了怎么办 幼儿把自己的舌头扣破皮了怎么办 6岁宝贝烧到39度怎么办 2岁半的宝宝说话结巴怎么办 2周3宝宝不会说话胆小怎么办 宝宝我2岁多了说话有点结巴怎么办 两岁宝宝说话突然结巴了怎么办 两岁3宝宝叫她名字不理人怎么办 九个月的宝宝身高不达标怎么办 3岁宝宝又吐又拉怎么办 宝宝发烧39度怎么办手脚很烫 两岁宝宝吃什么吐什么怎么办 7岁宝宝吃多了吐怎么办 7个月的宝宝大便干燥怎么办 10个月宝宝便秘大便干燥怎么办 一岁半宝宝老是拉糊糊状大便怎么办 外阴部长了一个疙瘩有点痒怎么办 小孩打架被另一个小孩家人告怎么办 德保豆浆机有电但不工作怎么办 刚买的笔记本c盘不足怎么办 qq糖粘在喉咙气管里怎么办 穿上旗袍后感觉后腰处不平整怎么办 机打票给客人给错联怎么办?急 ps修证件照感觉不太立体怎么办 手机百度上下载的文档打不开怎么办 5岁宝宝乘飞机没带证件怎么办