C++11多线程从join到unique_lock
**
前言
**
C++内容还是很复杂的,一般的时候使用的都是C的语法,能用到类和模板的内容都少见。
仅仅是使用C的内容而用C++做项目这也太浪费了,必须再进阶学习一下。继承、多态、封装都需要进一步的熟悉,至少在一个复杂到有几十步的任务里一定需要。
**
thread多线程对象的使用
**
多线程用的头文件有 、 等。
我曾经用CreateThread()(大概是这个函数)使用过多线程,但是这个函数仅仅是Windows下的,其他平台的多线程API都不同。显然如果根据不同的平台写多线程程序,就太麻烦了。
直到C++11的发布,C++11标准让C++至少在多线程方面可以跨平台编译了。
简单示例
#include <iostream>
#include<thread>
using namespace std;
void MyPrint() {
cout << "heiehie" << endl;
}
int main(){
thread myThread(MyPrint);
myThread.join();
cout << "end" << endl;
}
- MyPrint是线程入口函数,当thread对象的构造函数执行完后线程就开始了。
- join()是主线程等待子线程执行完再执行。
- detach()是分离主线程和子线程,这种情况下,主线程执行完后,子线程还在执行。
- joinable()判断是否有使用过join()或detach()
一般使用多线程都是用join()。而使用detach()会冒出各种各样的错误。
使用detach()时,如果主线程执行完,局部变量或对象的内存被释放,而子线程还在执行,这样就会出现错误…
一般不使用detach(),但是就怕万一。所以,关于detach()的一些坑还是了解一下比较好。
thread构造传参
构造函数传递的参数可以是简单的入口函数,也可以是成员函数、类对象(重载了括号)、lambda表达式等。
非成员函数传递与类对象传递
下面这个示例中函数的参数个数任意。在传入时,入口函数或类对象后面写参数。如果没有参数,就只传递类对象或入口函数。
#include <iostream>
#include<thread>
using namespace std;
class TA {
public:
void operator()(int num,int num2,int num3 = 1 ) {
cout << "类对象传递了 & num = "<<num<<" num2 = "<<num2 <<" num3 = "<<num3<< endl;
}
};
void MyPrint(int num) {
cout << "入口函数传递了 & num = " << num << endl;
}
int main(){
TA ta;
thread myThread(ta,3,2);
//thread myThread(MyPrint,3);
myThread.join();
cout << "end" << endl;
}
当传递类对象进入线程时,实际上是将类对象拷贝到了子线程中。
为了证明这个,下面在类中写上拷贝构造函数。
#include <iostream>
#include<thread>
using namespace std;
class TA {
public:
int& m_i;
TA(int& a) :m_i(a) { cout << "构造函数被执行" << endl; }
TA(const TA& a) :m_i(a.m_i) { cout << "拷贝构造函数被执行" << endl; }
~TA() { cout << "析构函数被执行" << endl; }
void operator()(int num,int num2,int num3 = 1 ) {
cout << "类对象传递了 & num = "<<num<<" num2 = "<<num2 <<" num3 = "<<num3<< endl;
cout << "m_i >>>" << m_i << endl;
}
};
void MyPrint(int num) {
cout << "入口函数传递了 & num = " << num << endl;
}
int main(){
int i = 10;
TA ta(i);
i = 11;//更改i的值
thread myThread(ta,3,2);
//thread myThread(MyPrint,3);
myThread.join();
cout<<"wait"<<endl;
cout << "end" << endl;
}
这里除了证明传递类对象有拷贝外,还有一个需要注意的点。
上面在进行值传递的时候,用的是引用。本来在创建线程后传递进去是10,但显示时是11。显然 i 是共享数据,这样的情况是很危险的。
如果将上面代码中的join()改成detach(),就出现因为主线程提前结束, i 的内存被释放而出现异常。
如果是不想用共享数据的话,当然最好不用。
- 值的传递最好就只用值传递的方式。
那么什么情况下值的传递可以用引用呢?
只需要在传递进去时,类型是const就没问题了(当然,还是希望值传递。最好还是不用detach()).
对于字符串,需要使用临时对象。考虑到创建线程时传递参数的步骤是拷贝后再执行子线程,万万不可在入口函数里转换字符串类型---------参数传递过去后线程就开始了,如果主线程先结束,入口函数的参数可能还没来得及转换就被删除了。
void MyPrint( const int& i,const string& bufString) {
cout << "入口函数传递了 & i = " << i <<" & bufString = "<<bufString.c_str()<< endl;
}
int main() {
int a = 1;
char buf[] = "this is a string";
thread myThread(MyPrint,a,string(buf));
myThread.detach();
cout << "wait" << endl;
cout << "end" << endl;
}
关于临时对象的传递,除了使用类(参数) 方法外,自然还可以先创建类对象,然后直接传递类对象(这里的类对象也是个临时的)。
#include <iostream>
#include<thread>
using namespace std;
class A {
public:
mutable int m_i;
A(int a) :m_i(a) { cout << "构造函数执行" << endl; }
A(const A& a) :m_i(a.m_i) { cout << "拷贝构造函数执行" << endl; }
~A() { cout << "析构函数执行" << endl; }
};
void MyPrint( const A& buf) {
buf.m_i = 55;
cout << "入口函数传递了 & buf.m_i = "<<buf.m_i<<"& buf = "<<&buf<< endl;
cout << "子线程ID>>>" << std::this_thread::get_id() << endl;
}
int main() {
cout << "主线程ID>>>" << std::this_thread::get_id() << endl;
A a(100);
thread myThread(MyPrint,a);
myThread.join();
cout << "a.m_i = " << a.m_i << endl;
cout << "wait" << endl;
cout << "end" << endl;
}
上面mutable保证类对象的值是可修改的,但是主线程中的值并没有被修改。如果我是需要用子线程修改主线程中的值的话---------也就是用共享数据-------我就不能让临时对象的传递后又拷贝,如果传递过去是真的引用,那就可以同时修改了。
那么不需要mutable类型了,还需要在创建线程时修改一点,std::ref保证传递过去没有拷贝。
thread myThread(MyPrint,std::ref(a));
只有特殊情况才用这个方法,万不得已时。
使用了这种方法后,如果主线程中的变量、类对象被释放后又是一样的问题。
当然这个问题都是对于detach()而言的,如果使用std::ref(),后面就只能用join()
lambda表达式传递
lambda表达式也是C++11新引入的技术。
可以用来编写内嵌的匿名函数来替代函数对象和非成员函数。
下面是lambda表达式一个简单的演示。
auto myLambda = [] {
cout << "线程开始执行了" << endl;
};
thread myThread(myLambda);
智能指针传递
void MyPrint(unique_ptr<int > p ){
}
int main() {
unique_ptr<int> pCurrent(new int(100));
thread myThread(MyPrint, std::move(pCurrent));
myThread.detach();
cout << "wait" << endl;
cout << "end" << endl;
}
std::move是智能指针操作,意思是传递指针所有权。
成员函数传递
#include <iostream>
#include<thread>
using namespace std;
class A {
public:
mutable int m_i;
A(int a) :m_i(a) { cout << "构造函数执行" << endl; }
A(const A& a) :m_i(a.m_i) { cout << "拷贝构造函数执行" << endl; }
~A() { cout << "析构函数执行" << endl; }
void Inter_Work(int num) {
cout << "子线程成员函数执行 & num = "<<num << endl;
}
};
int main() {
A ta(10);
thread myThread(&A::Inter_Work, ta,13);
myThread.detach();
cout << "wait" << endl;
cout << "end" << endl;
}
同样,如果在类对象传递时加std::ref(),又会不进行拷贝。
- 这种情况下,还可以用& 替代std::ref()
数据共享分析与互斥量
对于共享数据,如果我的子线程要读它,多个线程同时执行都没问题。但是如果有的线程要写,有的线程要读,这样错误就出现了。
所以解决办法是,有的线程读/写操作时,其他线程等着。
#include <iostream>
#include<thread>
#include<list>
#include<mutex>//互斥量
using namespace std;
/*---------------------------
//共享数据的保护案例代码
--------------------------------------*/
class A {
private:
std::list<int> msgRecvQueue;//专门用于代表玩家给我们发的命令 , 共享数据
std::mutex myMutex;//我的锁
public:
//线程1 ,把收到的玩家命令dump到一个队列中
void inMsgRecvQueue() {
for (int i = 0; i < 100; ++i) {
cout << "#inMsgRecvQueue()执行,插入一个元素>>" << i << "\n";
myMutex.lock();//锁住
msgRecvQueue.push_back(i);
myMutex.unlock();//解锁
}
cout << "#end inMsgRecvQueue" << "\n";
}
bool outMsgLULProc(int& command) {
myMutex.lock();//锁住
if (!msgRecvQueue.empty()) {
//不为空
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
myMutex.unlock();//解锁
return true;
}
myMutex.unlock();//解锁
return false;
}
//线程2 , 把命令队列中数据取出
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100; ++i) {
bool result = outMsgLULProc(command);
if (result) {
cout << "#outMsgLULProc()执行,取出一个元素>>" <<i << "\n";
//这里就考虑处理数据
//.....
}
else {
cout << "#消息队列为空!>>" << i << "\n";
}
}
cout << "#end outMsgRecvQueue" << "\n";
}
};
int main(){
//创建两个线程
A myobj_a;
thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj_a);
thread myInnMsgObj(&A::inMsgRecvQueue, &myobj_a);
myOutnMsgObj.join();
myInnMsgObj.join();
cout << "#end main" << endl;
}
互斥量mutex保证只有一个线程可以拿到了“锁”。只有等它解锁,其他线程才能去“抢”。
有加锁,必然需要解锁。加锁和解锁间的模块就是操作共享数据的模块。非共享数据千万不要放到模块里,这样会降低程序运行效率。
如果我不想unlock怎么办?
加个锁就要解个锁,就像new一个对象还有释放它。这样的操作实在是太麻烦了。为了不要自己unlock,需要使用std::lock_guard替代。
std::lock_guard是类模板,使用这个会自动加锁和解锁,解锁操作发生在其析构函数里。所以,让lock_guard什么时候析构,即控制lock_guard的生命周期,就可以包含不同数目的操控共享数据的模块。
lock_guard的使用
像下面这种使用方式,函数返回后lock_guard就析构了。
bool outMsgLULProc(int& command) {
std::lock_guard<std::mutex> sbguard(myMutex);
//myMutex.lock();//锁住
if (!msgRecvQueue.empty()) {
//不为空
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//myMutex.unlock();//解锁
return true;
}
//myMutex.unlock();//解锁
return false;
}
或者是用局部作用域限定的方法。
{
std::lock_guard<std::mutex> sguard2(myMutex);
msgRecvQueue.push_back(i);
}
只是这是方法不怎么灵活。
死锁
在多把锁的情况下,如果有多个线程,线程A拿到一个锁,同时线程B拿到另一个锁,而只有两个锁都拿到线程才能执行下去,那么此时就会出现死锁。
解决办法可以是让每个线程的加锁顺序一致,都一起去“抢”一把锁,“抢”到第一把的只有一个线程,然后让它再去拿第二把锁。
第二个解决方法是用std::lock,功能是如果线程A拿到第一把锁,但是没拿到第二把锁,那么就把第一把锁放开。
std::lock(myMutex1, myMutex2);
std::lock_guard<std::mutex> s1(myMutex1, std::adopt_lock);
std::lock_guard<std::mutex> s2(myMutex2,std::adopt_lock);
上面的std::lock已经给两个互斥量加锁了,下面的lock_guard除了省去unlock的麻烦外,还有一点。std::adopt_lock的标记表示lock_guard不再加锁。
unique_lock取代lock_guard
简单的unique_lock的使用和lock_guard一样。
将代码中的lock_guard替换成unique_lock都一样。unique_lock的优点就是特别灵活。
- std::adopt_lock标记表示互斥量表示已经被lock了,不用再lock
- std::try_to_lock标记表示尝试lock,如果没有成功也会立即返回,不会阻塞。
- std::defer_lock标记。前提你不能先lock。表示并没有给mutex加锁,初始化了一个没有加锁的mutex
下面是用try_to_lock的演示
std::unique_lock<std::mutex> s1(myMutex1, std::try_to_lock);
if (s1.owns_lock()) {
msgRecvQueue.push_back(i);
}
else {
cout << "#inMsgRecvQueue()执行,但是没有拿到锁.....T_T>>" << i << "\n";
}
和这个有相同功能的是unique_lock的对象try_lock(),但是此时标记要为std::defer_lock
这种没有拿到锁也不会阻塞的方法没什么用。
当在另一个入口函数中的共享数据处理的代码中加入下面的代码做延迟时,这个使用了try_to_lock标记的入口函数就很难在拿得到锁。
//process delay
std::chrono::milliseconds dura(2000);//ms
std::this_thread::sleep_for(dura);//休息一定的时长
在defer_lock的情况下
由于std::defer_lock是创建没有加锁的互斥量mutex,所以需要另外再手动加锁。
try_lock() 功能和std::try_to_lock功能一样。
std::unique_lock<std::mutex> s1(myMutex1, std::defer_lock);//创建一个没有加锁的mutex
//调用unique_lock的lock(),不用关心其解锁
if (s1.try_lock()) { //尝试加锁
msgRecvQueue.push_back(i);
}
else {
cout << "#inMsgRecvQueue()执行,但是没有拿到锁.....T_T>>" << i << "\n";
}
unlock()和lock(),如果是在unique_lock对象创建后使用了lock,本来是不用自己unlock的。
用unlock的原因是如果需要在s1析构前我还要执行其他非共享数据的操作代码的话,就需要在析构前解锁,执行别的内容,再加锁。
std::unique_lock<std::mutex> s1(myMutex1, std::defer_lock);//创建一个没有加锁的mutex
s1.lock();
//因为非共享代码要处理
s1.unlock();
//这里处理一些非共享代码
//.....
s1.lock();
msgRecvQueue.push_back(i);
最后还有一个release() 转移互斥量的所有权。
转移后unique_lock对象就空了,其解锁需要靠传递出去的mutex*指针。
std::unique_lock<std::mutex> s1(myMutex1);//这里就加锁了
std::mutex * ptx = s1.release();/*现在ptx有责任追究解锁了*/
msgRecvQueue.push_back(i);
ptx->unlock();//解锁
这个总结感谢b站https://www.bilibili.com/video/av39171692/?p=3的****,其为了推广 http://www.51itstudy.com/和帮助C++爱好者学习而录的教程。
注:这个网站不是视频中的www.51studyit.com,那个网站yum可能是失效了,估计是更改域名了。
如果有错希望得到指正。