关于C++11共享数据带来的死锁问题的提出与解决
举个例子,如果有一份资源,假如为list<int>资源,假设有两个线程要对该资源进行压入弹出操作,如果不进行锁的话,那么如果两个线程同时操作,那么必然乱套,得到的结果肯定不是我们想要的结果。于是引入了锁的机制,当一个线程进行相应操作之前加一把锁,访问结束后再释放锁,那么问题便可得到解决,但是会存在一个隐患:
比如线程A对该资源上了一把锁x,又上了一把锁y,然后对资源i进行弹出元素,这时候又有了另外一个线程B,他想要进行压入操作,那么他得等到线程A弹出操作结束并释放锁才行,假设线程A 的弹出操作结束了,释放了锁,于是轮到线程B进行压入操作,同样他也需要进行加锁,他的顺序是先上锁y,然后上锁x,但是很不巧的是,在他上完锁y的时候,线程A立马上了一个锁x,于是线程B想要用锁x就无能为例,只有等到线程A释放锁x,于是他在这里等待着,与此同时,线程A为了要进行弹出操作,他还缺一把锁y,只有等到线程B释放锁y才能够进行相应的操作,最后两个线程都在那里等待着对方,就那么干等下去。。。。
假设线程B的加锁顺序和线程A一样,都是先上锁x,在上锁y,那么再来看情况如何:回到第一次线程A弹出操作并释放了两个锁,然后线程B进行压入操作,他先上了锁x,如果这时候线程A也想要争夺资源的操作权,来上锁的话,他的顺序也是先上锁x,这时候由于线程B用了锁x,于是只有等到线程B释放锁x才能够继续进行下去,于是线程B就不存在被线程A 上锁的情况,所以也就不存在死锁了。
接下来对上面的文字进行代码演示:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{ if (!msl.empty())
{
mut_one.lock();
mut_two.lock();
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
mut_one.unlock();
mut_two.unlock();
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
mut_two.lock();
mut_one.lock();
msl.push_back(i);
cout << "压入的元素为:" << i << endl;
mut_two.unlock();
mut_one.unlock();
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
结果很快停止:
把锁的顺序改下:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{ if (!msl.empty())
{
mut_one.lock();
mut_two.lock();
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
mut_one.unlock();
mut_two.unlock();
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
mut_one.lock();
mut_two.lock(); msl.push_back(i);
cout << "压入的元素为:" << i << endl;
mut_two.unlock();
mut_one.unlock();
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
输出结果不存在死锁的情况:
上述对上锁的顺序有一定的要求,但是对解锁的顺序并没有要求,仔细想想是不是这样?
接下来回到上锁的问题,能不能有一个更好的方法,不用担心顺序的问题,假设把刚才那个问题换个角度考虑,假设如下:对于list<int>的操作,虽然我要上两把锁才能够对资源进行操作,但是有一个前提条件,我必须同时上锁,也就是要么不上锁,要么都上锁(要么不上锁,也就是当我上了一把锁的时候,如果另一把锁被占用,我便会释放之前的锁。要么都上锁,也就是如果另一把锁没被占用,我继续上锁)。细想想看如果有一个这种方案便不会因为顺序问题而导致死锁的产生,万幸的确有这么个函数,他就是用来处理顺序问题的,也就是std::lock(锁1,锁2.....).下面代码演示:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{ if (!msl.empty())
{
std::lock(mut_one,mut_two);
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
mut_one.unlock();
mut_two.unlock();
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
std::lock(mut_one, mut_two);
msl.push_back(i);
cout << "压入的元素为:" << i << endl;
mut_two.unlock();
mut_one.unlock();
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
输出效果不会产生死锁:
虽然上锁的顺序问题解决了,但是细看代码,还是会有问题的产生,如果忘记了解锁怎么办?这个问题幸好也有函数帮我们解决了,也就是接下来要说的:lock_guard,
guard是门卫的意思,顾名思义,这个lock_guard大概是和锁的管理有关,但是他不是函数,而是一个模板类,他是为了保证锁的释放的,有了他,我们不需要人为的手动解锁,
它会在析构的时候自动帮助我们进行解锁操作,下面对上述代码再进一步改进:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
if (!msl.empty())
{
std::lock(mut_one,mut_two);
std::lock_guard<std::mutex> lo_one(mut_one,std::adopt_lock);
std::lock_guard<std::mutex> lo_two(mut_two,std::adopt_lock);
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
std::lock(mut_one, mut_two);
std::lock_guard<std::mutex> lo_one(mut_one,std::adopt_lock);
std::lock_guard<std::mutex> lo_two(mut_two,std::adopt_lock);
msl.push_back(i);
cout << "压入的元素为:" << i << endl;
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
输出同样正确(图略)
对于lock_guard(mut_one,std::adopt_lock);有必要再解释一下。
回到代码:
std::lock(mut_one,mut_two);
std::lock_guard<std::mutex> lo_one(mut_one,std::adopt_lock);
std::lock_guard<std::mutex> lo_two(mut_two,std::adopt_lock);
如果lo_one和lo_two只有一个参数,那么lo_one里的参数mut_one表示,在模板类lock_guard调用构造函数的时候,会调用互斥量mutex的lock:
相当于:std::mutex mut_one; mut_one.lock();
在模板类lock_guard调用析构函数的时候,会调用mutex的unlock:
相当于:mut_one.unlock()。
对于lo_two同样如此;也就是像下面这样:
std::lock_guard<std::mutex> lo_one(mut_one);
std::lock_guard<std::mutex> lo_two(mut_two);
但是之前的含有两个参数,接下来看第二个参数,他的作用是告诉lock_guard,在构造函数中,不用调用互斥量的lock()函数。
至此,对于lock_guard的简单应用就到此,在这里可以看出,他得配合着lock()来使用,倘若前面的lock没有,只是单独的lock_guard,虽然他解决了解锁的问题,但是对于上锁的顺序问题还是无能为力.
对于lock_guard,感觉用起来并不是那么灵活,接下来要说的unique_lock,相比较于lock_guard,他不但拥有lock_guard的全部功能,并在此基础上对其进行了扩展,使用起来也就更加灵活了。
std::lock(mut_one,mut_two);
std::lock_guard<std::mutex> lo_one(mut_one,std::adopt_lock);
std::lock_guard<std::mutex> lo_two(mut_two,std::adopt_lock);
把lock_guard换成unique_lock:
std::lock(mut_one,mut_two);
std::unique_lock<std::mutex> lo_one(mut_one,std::adopt_lock);
std::unique_lock<std::mutex> lo_two(mut_two,std::adopt_lock);
执行情况和lock_guard并无差别,拥有同样的功能,接下来说说其不一样的地方:他不一样的地方就在第二个参数,不但拥有std::adopt_lock,还可以填其他参数,接下来从问题出发,逐一探讨.
把上面的代码进行变动,如下:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
std::chrono::milliseconds dura(20000);
std::this_thread::sleep_for(dura);
if (!msl.empty())
{
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
msl.push_back(i);
cout << "压入的元素为:" << i << endl;
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
上面代码我让其中一个上锁后睡眠了一段时间,那么另外一个线程只好等待,如果等待时间过长,那么对于另外一个线程来说,无疑是一种浪费,能不能让他在等待的时候做其他的事情,
于是引入unique_lock的第二个参数换成std::try_to_lock便可以解决如上问题,如下代码所示:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
if (!msl.empty())
{
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one,std::try_to_lock);
if (lo_one.owns_lock())
{
//拿到锁执行的操作
msl.push_back(i);
cout << "压入的元素为:" << i << endl;
}
else
{
//如果没有拿到锁的话,让他执行其他操作
//操作.............................
cout << "没有拿到锁" << data++<<endl;
}
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata,&a);
std::thread th_two(&A::popdata,&a);
th_one.join();
th_two.join(); return 0;
}
总结下就是try_to_lock尝试lock,如果lock不成功,也不会在那干等待,可以去做其他的事情.
如果对于一段资源,我希望他的一部分是加锁的,一部分不需要加锁,那么又该怎么办?于是便有了std::defer_lock,如下代码所示:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
if (!msl.empty())
{
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{ for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one, std::defer_lock);
lo_one.lock();
//共享资源代码段
lo_one.unlock();
//非共享部分,不希望加锁 lo_one.lock();//用完之后不要忘记加锁
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata, &a);
std::thread th_two(&A::popdata, &a);
th_one.join();
th_two.join(); return 0;
}
对于defer_lock,他的功能是把互斥量和对象绑定在一起,但是并不加锁,在需要的时候对他进行加锁,加完锁可以不用考虑解锁,但是手动解锁了必须要加锁。
对于defer_lock,它还具有类似try_to_lock的功能,它也可以尝试加锁,在拿不到资源的情况下做别的事情,如下代码所示:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
if (!msl.empty())
{
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{ for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one, std::defer_lock);
if (lo_one.try_lock == true)
{
//拿到锁
}
else
{
//没有拿到锁
} //lo_one.lock();
////共享资源代码段
//lo_one.unlock();
////非共享部分,不希望加锁 //lo_one.lock();//用完之后不要忘记加锁
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata, &a);
std::thread th_two(&A::popdata, &a);
th_one.join();
th_two.join(); return 0;
}
接下来看release,直接看代码:
// ConsoleApplication5.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<mutex>
#include<list>
using namespace std; class A
{
public:
A(int n=0) :data(n) {}
void popdata()
{
for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one);
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
if (!msl.empty())
{
int command = msl.front();
msl.pop_front();
cout << "弹出的元素为:" << command << endl;
}
}
} void endata()
{ for (int i = 1; i <= 1000; i++)
{
std::unique_lock<std::mutex> lo_one(mut_one); std::mutex* p = lo_one.release();//解除unique_lock与mut_one的关系
//由于解除关系之前是上了锁的,解除关系了,后面的解锁必须手动完成。
msl.push_back(i);
p->unlock();
}
}
private:
int data;
list<int> msl;
std::mutex mut_one;
std::mutex mut_two;
};
int main()
{
A a(1); std::thread th_one(&A::endata, &a);
std::thread th_two(&A::popdata, &a);
th_one.join();
th_two.join(); return 0;
}
release的作用是用来释放mutex与unique_lock的关系的,他返回会一个mutex的指针,所以对于共享资源,如果前面加了锁,在释放了与mutex的关系后,必须手动进行解锁。
再看下面代码:
std::unique_lock<std::mutex> lo_onea(mut_one);
std::unique_lock<std::mutex> lo_one(std::move(lo_onea));
上面的代码是可以运行的,说明绑定的unique_lock可以进行转移,上面代码就是将mut_one由原来的lo_onea转移给了lo_one来接管.
最后补充一下,对于资源的资源也可以通过函数的返回值进行,比如上面的代码可以改写:
class A
{
public:
std::unique_lock<std::mutex> foo()
{
std::unique_lock<std::mutex> my(mut_one);
return my;
}
void ss()
{
std::unique<std::mutex> a=foo();
}
}
以上都是语法层面,蕴含的深刻道理还是得通过大量的实战才行。
关于C++11共享数据带来的死锁问题的提出与解决的更多相关文章
- Java并发基础06. 线程范围内共享数据
假设现在有个公共的变量 data,有不同的线程都可以去操作它,如果在不同的线程对 data 操作完成后再去取这个 data,那么肯定会出现线程间的数据混乱问题,因为 A 线程在取 data 数据前可能 ...
- Java笔记1 : 在生产者消费者模式中,线程通信与共享数据,死锁问题与解决办法
本例定义了4个类,这里说一下,方便下面讲解.分别是Product(产品),Producer(生产者),Consumer(消费者), Test(测试类). 多线程之间通信与共享数据只要引用同一内存区域就 ...
- C++11并发编程4------线程间共享数据
举个例子: 刚参加工作的你,只能租房住,嫌房租贵就和别人合租了,两个人住一起只有一个洗手间,每天早上起床的时候,如果你室友在洗手间,你就只能等着,如果你强行进去,那画面就不可描述了.同样的问题,如果多 ...
- JAVA笔记14__多线程共享数据(同步)/ 线程死锁 / 生产者与消费者应用案例 / 线程池
/** * 多线程共享数据 * 线程同步:多个线程在同一个时间段只能有一个线程执行其指定代码,其他线程要等待此线程完成之后才可以继续执行. * 多线程共享数据的安全问题,使用同步解决. * 线程同步两 ...
- Python 进程之间共享数据
最近遇到多进程共享数据的问题,到网上查了有几篇博客写的蛮好的,记录下来方便以后查看. 一.Python multiprocessing 跨进程对象共享 在mp库当中,跨进程对象共享有三种方式,第一种 ...
- 在 App 扩展和主 App 间共享数据
tags: iOS 8,Swift,App Groups 随着 iOS 8 的发布,苹果为广大开发者很多新的 API,其中最突出显著的就非 App Extension 莫属了.这为开发者们又带来了很多 ...
- 详解 Qt 线程间共享数据(用信号槽方式)
使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...
- Java基础知识强化102:线程间共享数据
一.每个线程执行的代码相同: 若每个线程执行的代码相同,共享数据就比较方便.可以使用同一个Runnable对象,这个Runnable对象中就有那个共享数据. public class MultiThr ...
- Android应用程序组件Content Provider在应用程序之间共享数据的原理分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6967204 在Android系统中,不同的应用 ...
随机推荐
- python 豆瓣top250
豆瓣电影 import re import requests headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; ...
- ACwing1208. 翻硬币
题目: 小明正在玩一个"翻硬币"的游戏. 桌上放着排成一排的若干硬币.我们用 * 表示正面,用 o 表示反面(是小写字母,不是零). 比如,可能情形是:**oo***oooo 如果 ...
- SpringBoot项目存放微信的验证文件,把微信的验证文件放在根目录下
我们做微信开发的时候,有时候会有如下要求 用户在网页授权页同意授权给公众号后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠. 下载文件 将以下文件上传至填写域名或路径指向的w ...
- 【LeetCode】1402. 做菜顺序 Reducing Dishes
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 贪心 日期 题目地址:https://leetcode ...
- BD String
BD String Accepts: 388 Submissions: 1164 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/ ...
- 【操作系统】 DOS命令windows批处理batch编程——第一章
参考网址: http://docs.30c.org/dosbat/index.html 很多情况下,我们只需要记住一条命令 help ,就能掌握整个DOS命令.比如直接输入 help 可以得到命令的帮 ...
- A ROBUST KERNEL PCA ALGORITHM
目录 引 主要内容 问题一 问题二 Lu C, Zhang T, Du X, et al. A robust kernel PCA algorithm[C]. international confer ...
- 使用pypy3加速python运行
从这里下载对应OS版本的安装包 解压: tar xf pypy-x.y.z.tar.bz2 然后通过./pypy-x.y.z/bin/pypy可以直接进入console 可以使用pip安装包: ./p ...
- Java EE数据持久化框架mybatis练习——获取id值为1的角色信息。
实现要求: 获取id值为1的角色信息. 实现思路: 创建角色表sys_role所对应的实体类sysRole. package entity; public class SysRole { privat ...
- 编写Java程序_输入一个5位数,判断它是不是回文数。即12321是回文数,个位与万位相同,十位与千位相同。
要求: 输入一个5位数,判断它是不是回文数.即12321是回文数,个位与万位相同,十位与千位相同. 实现代码: package kaoshi; import java.util.Scanner; pu ...