关于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();
}
}
以上都是语法层面,蕴含的深刻道理还是得通过大量的实战才行。