相关知识点:
- join()和detach()只能使用一次,且不能同时混用( 一个线程对象只能使用一次join(),不然程序会崩溃;在线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式
)- joinable():判新是否可以成功使用join或者detach的; 返回true(可以join或者detach)或者false(不能使用)
- 线程对象的回调函数常用的有四种:普通函数、类的对象(这个对象以拷贝的形式传进去的)、类的成员函数(要加地址符号&)、Lambda 函数,也可以有其他的比如 :重载了operate ()的类对象
- 线程函数的参数(也就是回调函数的参数)是以值拷贝的方式拷贝到线程栈空间中的,其实际引用的是线程栈中的拷贝,而不是外部实参。(也就是说创建thread对象进行绑定时,哪怕你的形参是引用都不会改变当前函数的变量值,因为会发生拷贝)
- 线程函数的参数 是 类的对象 时,回调函数对应的接收参数使用规则是:const + 引用& 的方式接收。(3.1有实例)
第一部分:线程创建
C++11 线程库介绍
C++11 中新增了多线程 std::thread,相比之前使用较多的是操作系统提供的 POSIX 线程接口,新标准引入了线程库无疑带来了许多便利。
要使用 C++11 多线程,首先 gcc 编译器版本需要大于4.8,并且编译时,需要加上参数 -std=c++11 -lpthread
,可见,C++11 的线程是对 POSIX 线程的封装。
C++11 线程创建
在每个 C++ 应用程序中,都有一个默认的主线程,即 main 函数。在 C++11 中,可以通过创建 std::thread 类的对象来创建额外的线程。每个 std::thread 对象都可以与一个线程相关联。
需要引入的头文件:
#include <thread>
std::thread 对象构造的时候接受什么参数?
构造时需要给 std::thread 对象一个回调函数,该回调函数将在新线程启动时执行。回调函数可以是如下类型函数:
- 普通函数
- 类的成员函数
- Lambda 函数
- 类的对象
线程对象的创建如下:
std::thread t(cb);
新的线程将在创建新对象之后立即启动,并执行传入的回调函数。
此外,任何线程都可以调用该线程对象的 join()
函数来等待该线程退出。
下面示例中,主线程创建了一个新线程,创建这个新线程后,主线程会在控制台打印一些数据,然后等待新创建的线程退出。使用了三种不同的回调函数创建线程。
-
• 使用 普通函数 创建线程
#include <iostream> #include <thread> void thread_fun() { for (int i = 0; i < 5; i++) { std::cout << "child thread output:" << i << std::endl; } } int main() { std::thread t1(thread_fun); for (int i = 0; i < 5; i++) { std::cout << "main thread output:" << i << std::endl; } t1.join(); std::cout << "main thread exit." << std::endl; return 0; }
-
• 使用 类的成员函数 创建线程
#include <iostream> #include <thread> #include <functional> class MyPrint { public: void print() { for (int i = 0; i < 5; i++) { std::cout << "child thread output:" << i << std::endl; } } }; int main() { MyPrint mp; std::function<void(void)> func(std::bind(&MyPrint::print, &mp)); std::thread t1(func); //或如下一行的方式,等同于21、22俩行 std::thread t1(&MyPrint::print, &mp);//传入成员函数地址、 类对象的引用 //(如果后边detach了,尽量不要用类对象的引用,可以去掉&,直接mp,以值传递的方式来传参) for (int i = 0; i < 5; i++) { std::cout << "main thread output:" << i << std::endl; } t1.join(); std::cout << "main thread exit." << std::endl; return 0; }
-
• 使用 Lambda 函数创建线程
#include <iostream> #include <thread> int main() { auto func = []() //【】表示捕获列表,其中捕获的参数可用于函数体使用、()表示函数体的形参参数 { for (int i = 0; i < 5; i++) { std::cout << "child thread output:" << i << std::endl; } }; std::thread t1(func); for (int i = 0; i < 5; i++) { std::cout << "main thread output:" << i << std::endl; } t1.join(); std::cout << "main thread exit." << std::endl; return 0; }
-
• 使用 类的对象 创建线程
#include <thread> #include <iostream> #include <windows.h> using namespace std; class MM{ public: void operator()(){ //重载括号() cout<<"子线程启动"<<endl; } }; int main(){ MM mm;//对象充当线程处理函数 thread test1(mm); //上面也可以写成: //thread test1((MM()))//创建无名对象。MM()外要加括号,防止解析为函数。 test1.join();// cout<<"ILOVEYOU"<<endl; return 0; }
上面的示例运行结果如下:
$ ./test
main thread output:0
main thread output:1
main thread output:2
main thread output:3
main thread output:4
child thread output:0
child thread output:1
child thread output:2
child thread output:3
child thread output:4
main thread exit.
第二部分:线程回调函数的参数传递问题
- 导读知识点
注意:不要将 变量的地址 传递给线程的回调函数,如果有detach()情况下,因为主线程 1 中该局部变量可能已经出了作用域,而线程 2 仍尝试通过其传入的地址访问并操作它,有可能会导致以外的行为。如下程序:
#include <iostream>
#include <thread>
void newThreadCallback(int* p)
{
std::cout << "Inside Thread:p = " << p << std::endl;
std::chrono::milliseconds dura(1000);
std::this_thread::sleep_for(dura);
*p = 20;
std::cout << "Inside Thread:p = " << p << std::endl;
}
void startNewThread()
{
int i = 10;
std::cout << "Inside Main Thread: &i = " << &i << std::endl;
std::thread t(newThreadCallback, &i);
t.detach(); //此时主线程和子线程分离
std::cout << "Inside Main Thread: i = " << i << std::endl;
std::cout << "Range quit." << i << std::endl;//当该函数执行完以后会释放局部变量i的内存空间,而子线程此时如果没有完成,
//可能会继续使用变量i的地址来获取数据,程序就会发生未知错误
}
int main()
{
startNewThread();
std::chrono::milliseconds dura(3000);
std::this_thread::sleep_for(dura);
return 0;
}
同样,将指向位于 堆上的内存的指针 传递给线程时也要小心。因为新的线程在去访问它之前,某些线程可能会删除该内存,这种情况将导致程序崩溃掉。
-
ref()的作用:使得 回调函数 形参即使是通过拷贝复制来的,也能修饰外部实参(因为真的传了一个引用到线程函数里)
由于参数会被复制到新线程的堆栈,因此如果你要以引用的方式传递参数,需要对其进行检查,如下示例:
#include <iostream> #include <thread> void threadCallback(int const& x) { int& y = const_cast<int&>(x); y++; std::cout << "Child Thread x=" << x << std::endl; } int main() { int x = 9; std::cout << "Main Thread(begin) x=" << x << std::endl; std::thread t1(threadCallback, x); t1.join(); std::cout << "Main Thread(after) x=" << x << std::endl; return 0; }
运行结果:
$ ./test Main Thread(begin) x=9 Child Thread x=10 Main Thread(after) x=9
即使线程函数 threadCallback 接收引用参数,并且对参数进行了更改,但是在线程外是不可见的。这是因为线程函数 threadCallback 中的 x 是对新线程堆栈中复制的临时值的引用。
如何解决这个问题?使用 std::ref() ,更改程序如下:void threadCallback(int const& x) { int& y = const_cast<int&>(x); y++; std::cout << "Child Thread x=" << x << std::endl; } int main() { int x = 9; std::cout << "Main Thread(begin) x=" << x << std::endl; std::thread t1(threadCallback, std::ref(x)); //此处进行修改 t1.join(); std::cout << "Main Thread(after) x=" << x << std::endl; return 0; }
现在运行结果就正确了:
$ ./test Main Thread(begin) x=9 Child Thread x=10 Main Thread(after) x=10
1.获取线程ID
每个线程都有一个唯一 ID 与之关联,我们可以使用这个 ID 来识别线程。
this_thread::get_id(); //获取当前线程id
t1.get_id(); //获取指定线程id
2.线程(detach)时的坑
说明:主要是由 线程.detach() 引起的
一个新的线程启动,另一个线程就可以等待这个新线程完成。只需要调用新线程对象的 join() 函数即可。
std::thread th(thread_fun);
//...
th.join();
线程分离detach(有坑,注意细节)
分离的线程也成为守护线程或后台线程。将一个线程分离,只需要调用当前线程对象的 detach() 函数即可。
std::thread t(thread_fun);
t.detach();
-
调用 detach() 后,std::thread 对象不再与实际运行的线程相关联。如下实例:
void thread_fun() { std::cout << "Child Thread ID:" << std::this_thread::get_id() << std::endl; } int main() { std::thread t1(thread_fun); t1.detach(); std::cout << t1.get_id() << std::endl; t1.join(); return 0; }
创建了线程 t1 后,调用了线程的 detach 函数后,线程进行了分离,所以主线程不能再调用线程 t1 的方法,因为主线程和 t1 线程之间已经没有了联系。
在调用 join() 或 detach() 之前,我们应该每次都检查线程对象是否可以连接,即调用线程对象的 joinable() 函数,如下所示:
std::thread t1(thread_fun); t1.detach(); if (t1.joinable()) { t1.detach(); } if (t1.joinable()) { t1.join(); }
-
主线程detach后,回调函数的参数传递问题:
结论:数值传递时,使用值传递的方式,推荐const int i这种传递方式,尽量不要用引用的方式。 且指针传递一定会出错。如下例子
#include <iostream> #include <thread> using namespace std; void myPrint(const int& i, char* pmybuf) //推荐改为const int i { //如果线程从主线程detach了 //实际上i并不是mvar真正的引用,因为回调函数的参数是经过复制的值传递,即使主线程运行完毕mvar内存被释放, // 子线程用&i仍然是安全的,但仍不推荐传递引用 cout << i << endl; cout << pmybuf << endl;//指针在主线程detach时,绝对会有问题,因为主线程执行完mybuf的内存被释放, 地址就不存在了,这时用指针传递到回调函数里,就会出错 } int main() { int mvar = 1; int& mvary = mvar; char mybuf[] = "this is a test"; thread myThread(myPrint, mvar, mybuf);//第一个参数是函数名,后两个参数是回调函数的参数 //myThread.join(); myThread.detach(); cout << "Hello World!" << endl; }
修改后的代码如下:
#include <iostream> #include <thread> using namespace std; void myPrint(const int i, const string &pmybuf) //数值传递以值传递的方式完成,字符数组可以类似值传递的方式接受字符串 { cout << i << endl; cout << pmybuf << endl; } int main() { int mvar = 1; int& mvary = mvar; char mybuf[] = "this is a test"; thread myThread(myPrint, mvar, string(mybuf));//将字符数组强制转换成字符串的形式,类似值传递的方式完成传参更安全 //myThread.join(); myThread.detach(); cout << "Hello World!" << endl; }
若线程创建时传递的参数有类的对象,比如第三个参数:类A,类中有成员属性i
thread myThread(myPrint, mvar, A(i));
此时在创建线程时直接在第三个参数位置, 创建临时对象A(i),这种方式安全点。而回调函数接收时,也应该用引用来接收:myPrint(const int i, const A &pmybuf) { 函数体 };总结:
- 如果传递int这种简单类型,推荐使用值传递,不要用引用
- 如果传递类对象,避免使用隐式类型转换,全部都是创建线程这一行就创建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象
- 终极结论:建议不使用detach
3.传递类对象、智能指针作为 线程函数的参数
-
传递类对象
#include <iostream> #include <thread> using namespace std; class A { public: mutable int m_i; //mutable 关键字:m_i即使是被const修饰,也可以被修改 A(int i) :m_i(i) {} }; void myPrint(const A& pmybuf) { pmybuf.m_i = 199; cout << "子线程myPrint的参数地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl; } int main() { A myObj(10); thread myThread(myPrint, myObj); // 类对象作为线程函数的参数 myThread.join(); //myThread.detach(); cout << "Hello World!" << endl; }
代码解析:
(代码21行)当类对象作为线程函数的参数时: 1.回调函数myPrint(**const** A **&** pmybuf)中引用不能去掉,如果去掉会多创建一个对象(浪费资源,即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象); 2. const也不能去掉,去掉会报错(所以在子线程中修改m_i的值不会影响到主线程); 3. 如果希望子线程中修改m_i的值影响到主线程,用ref(myobj)来修饰该对象即可,可以用thread myThread(myPrint, std::ref(myObj)); 这样const就是真的引用了,myPrint回调函数定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
总结: 当 类对象 作为线程函数参数时,回调函数中对应的参数:const 和 & 必须使用。
准确点说:const必须使用,&尽量使用
-
智能指针作为线程函数参数
#include <iostream> #include <thread> #include <memory> using namespace std; void myPrint(unique_ptr<int> ptn) { cout << "thread = " << std::this_thread::get_id() << endl; } int main() { unique_ptr<int> up(new int(10)); //独占式指针只能通过std::move()才可以传递给另一个指针 //传递后up就指向空,新的ptn指向原来的内存 //所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了 thread myThread(myPrint, std::move(up)); myThread.join(); //myThread.detach();
第三部分:锁(互斥量)
1、创建和等待多个线程
void TextThread()
{
cout << "我是线程" << this_thread::get_id() << endl;
/* … */
cout << "线程" << this_thread::get_id() << "执行结束" << endl;
}
//main函数里 vector threadagg是线程池
for (int i = 0; i < 10; ++i)
{
threadagg.push_back(thread(TextThread));
}
for (int i = 0; i < 10; ++i)
{
threadagg[i].join();
}
优点:把thread对象放入到容器threadagg中管理,对一次创建大量的线程并对大量线程进行管理有好处 。
缺点:多个线程执行顺序是乱的,跟操作系统内部对线程的运行调度机制有关
一块内存数据共享时的问题:1. 只有读时,是稳定安全的 2. 有读有写时,只能有一个线程起作用,这个线程读数据的时候,其他线程不能写;当这个线程写数据时,其他线程不能读。
2、互斥量(mutex)
-
基本概念
互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。 -
基本用法
- 包含#include 头文件
- lock(),unlock()使用步骤:
首先lock()一下 note:锁不成功的时候程序阻塞等待,直至资源被释放后能够完成上锁,程序才能继续执行 然后操作共享数据 最后unlock()一下 注意:lock()和unlock()要成对使用
-
lock_guard类模板
lock_guard<mutex> Myobj(myMutex); // 用于取代lock()和unlock(),使用lock_guard之后不能再用lock和unlock了
lock_guard创建对象Myobj后,其构造函数执行了mutex::lock(); 在作用域结束时(比如该对象在函数结束时要被释放),该类模板的对象自动调用其析构函数,执行mutex::unlock()。
-
std::lock()函数模板
std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
-
实例
#include <iostream> #include <thread> #include <list> #include <mutex> using namespace std; class A{ public: void inMsgRecvQueue() //函数作用:向消息队列里输入数据 { for (int i = 0; i < 100000; ++i) { cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl; { //在for的每次循环开始时,自动lock。for每次循环生命周期结束时,其生命周期结束,自动unlock //lock_guard<mutex> sbguard(myMutex); myMutex.lock(); msgRecvQueue.push_back(i); myMutex.unlock(); } } } bool outMsgLULProc() { myMutex.lock(); if (!msgRecvQueue.empty()) { cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl; msgRecvQueue.pop_front(); myMutex.unlock(); return true; } myMutex.unlock(); return false; } void outMsgRecvQueue() //函数作用:输出消息队列数据 { for (int i = 0; i < 100000; ++i) { if (outMsgLULProc()) { } else { cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl; } } } private: list<int> msgRecvQueue; //消息队列 mutex myMutex; }; int main() { A myobja; thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//以类函数的方法创建的线程 thread myInMsgObj(&A::inMsgRecvQueue, &myobja); myOutMsgObj.join(); myInMsgObj.join(); return 0; }
死锁
-
基本概念:
线程死锁描述的是这样一种情况:
多个线程同时被阻塞,它们中的一个或者全部都在等待某些资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 已经锁住了资源 2,线程 B 已经锁住了资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U70EY0ON-1686291470539)(assets/image-20230407212929-frk6qzu.png)]
-
造成死锁的实例:
在inMsgRecvQueue()中上锁时,先成功上锁2,后请求上锁1; 而在outMsgLULProc()函数中,先成功上锁1,后请求上锁2。
在两个线程中,由于1和2都被成功上锁了,而另一个线程却申请上锁另外一个,都会造成程序阻塞,且一直阻塞下去,那就会造成死锁。
当有多个线程要上锁时,上锁顺序很重要,不小心就会造成死锁,而解锁时的顺序无所谓。
#include <iostream> #include <thread> #include <list> #include <mutex> using namespace std; class A{ public: void inMsgRecvQueue() //函数作用:向消息队列里输入数据 { for (int i = 0; i < 100000; ++i) { cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl; { //使用lock_guard之后不能再用lock和unlock了。在for循环开始时,自动lock。for循环生命周期结束时,其生命周期结束,自动unlock //lock_guard<mutex> sbguard(myMutex1, adopt_lock); lock(myMutex1, myMutex2); //一次锁定多个互斥量,用于处理多个互斥量。 //myMutex2.lock(); //myMutex1.lock(); msgRecvQueue.push_back(i); myMutex1.unlock(); myMutex2.unlock(); } } } bool outMsgLULProc() { myMutex1.lock();//上锁时顺序很重要,可能会造成死锁 myMutex2.lock(); if (!msgRecvQueue.empty()) { cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl; msgRecvQueue.pop_front(); myMutex2.unlock(); myMutex1.unlock(); return true; } myMutex2.unlock();//解锁时,谁先解锁都无所谓 myMutex1.unlock(); return false; } void outMsgRecvQueue() //函数作用:输出消息队列数据 { for (int i = 0; i < 100000; ++i) { if (outMsgLULProc()) { } else { cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl; } } } private: list<int> msgRecvQueue; //消息队列 mutex myMutex1; mutex myMutex2; }; int main() { A myobja; thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//以类函数的方法创建的线程 thread myInMsgObj(&A::inMsgRecvQueue, &myobja); myOutMsgObj.join(); myInMsgObj.join(); return 0; }
-
避免造成死锁
方法一:为避免造成上述死锁情况发生,只需要每个线程的上锁顺序保持一致即可。解锁顺序无所谓。
方法二:或者使用std::lock(mutex1,mutex2……)防止死锁的发生。就无需考虑上锁顺序的问题。
代码20,21行 和 代码 31,32的上锁顺序保持一样即可。
#include <iostream> #include <thread> #include <list> #include <mutex> using namespace std; class A{ public: void inMsgRecvQueue() //函数作用:向消息队列里输入数据 { for (int i = 0; i < 100000; ++i) { cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl; { //使用lock_guard之后不能再用lock和unlock了。在for循环开始时,自动lock。for循环生命周期结束时,其生命周期结束,自动unlock //lock_guard<mutex> sbguard(myMutex1, adopt_lock); //lock(myMutex1, myMutex2); //防止死锁。 myMutex1.lock(); myMutex2.lock(); msgRecvQueue.push_back(i); myMutex1.unlock(); myMutex2.unlock(); } } } bool outMsgLULProc() { myMutex1.lock();//上锁时顺序很重要,可能会造成死锁 myMutex2.lock(); if (!msgRecvQueue.empty()) { cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl; msgRecvQueue.pop_front(); myMutex2.unlock(); myMutex1.unlock(); return true; } myMutex2.unlock();//解锁时,谁先解锁都无所谓 myMutex1.unlock(); return false; } void outMsgRecvQueue() //函数作用:输出消息队列数据 { for (int i = 0; i < 100000; ++i) { if (outMsgLULProc()) { } else { cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl; } } } private: list<int> msgRecvQueue; //消息队列 mutex myMutex1; mutex myMutex2; }; int main() { A myobja; thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//以类函数的方法创建的线程 thread myInMsgObj(&A::inMsgRecvQueue, &myobja); myOutMsgObj.join(); myInMsgObj.join(); return 0; }