声明:本文是在GitHub上找的代码,本文做了一些简单的增减。希望能够给想学习c++多线程的童鞋们提供一点帮助,当然也是为了自己以后复习的时候能够有个地方看以前写的东西。**
(一)线程/进程基本概念
1. 进程
进程是资源调度的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
2. 线程
线程是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
3. 并发(多线程)
多个线程同时执行,由于处理器CPU个数有限,不同任务之间需要分时切换,这种切换是有开销的,操作系统需要保持切换时的各种状态。线程一多大量的时间用于切换,导致程序运行效率低下。
3.1 实现方法
-
多进程实现并发
进程之间可以通过管道,文件,消息队列,共享内存进程通信
不同电脑之间通过socket通信
-
单进程中多线程
一个进程中的所有线程共享地址空间(共享内存),例如:全局变量,指针、引用都可以在线程之间传递,因此使用多线程的开销比较小。
共享内存引入数据不一致问题,需要使用锁进行解决
3. 2 线程VS进程
- 线程启动速度快,轻量级
- 线程的系统开销小
- 使用有一定难度,需要处理数据一致性问题
(二)C++线程入门
1. 主线程与子线程
-
主线程执行完毕,整个进程也执行完毕,一般情况下,如果其他子线程没有执行完毕,那么这些子线程也会被操作系统强行终止。
-
子线程执行一个函数,函数运行完毕,子线程随之运行完毕。
-
一般情况下,如果想保持子线程的运行状态,需要保持主线程持续运行。
#include <iostream>
#include <thread>
using namespace std;
void myprint(){
cout<< "子线程开始" <<endl;
//...子线程操作
cout << "子线程结束" << endl;
}
int main(){
//执行两个操作,1.新建一个线程,入口为myprint 2. 执行线程
std::thead td(myprint);
if(td.joinable()){ //判断线程能否join或detach
td.join();
}
//join()会阻塞线程,在子线程执行完毕之后,在与主线程汇合
//如果没有join,那么主线程结束而子线程未结束,则会报错
//td.detach(); //一旦detach则不能再join
cout << "主线程结束" <<endl;
return 0;
}
- 传统多线程,主线程需要要等待子线程结束之后才能结束。
- **join()**会阻塞线程,在子线程执行完毕之后,再与主线程汇合
- 新特性,使用**detach()**方法可以做到主线程和子线程分离,主线程可以先结束,而子线程继续运行。一旦detach()之后,主线程关联的Thread就会与主线程失去关联,子线程会驻留在后台运行,此时子线程由运行时库接管,子线程执行完后,由运行时库(守护线程)清理相关资源。
- 其他创建线程的方法
#include <iostream>
#include <thread>
using namespace std;
//使用类对象(可调用对象)创建线程
class TA{
public:
int &m_i; //不建议在线程中使用引用和指针,这样detach时,主线程结束,相关变量被释放,导致子线程中使用不可预知的值
TA(int &i): m_i(i){
cout<< "构造函数开始执行" <<endl;
}
TA(const TA &ta): m_i(ta.m_i){
cout<< "拷贝构造函数开始执行" <<endl;
}
~TA(){
cout<< "析构函数开始执行" <<endl;
}
void operator()(){ //不能带参数
cout<< "子线程开始" <<endl;
//...子线程操作
cout << "子线程结束" << endl;
}
};
int main(){
int myi = 6
TA ta(myi);
//执行两个操作,1.新建一个线程,入口为myprint 2. 执行线程
std::thead td(ta); //自动调用operato
if(td.joinable()){ //判断线程能否join或detach
td.join();
}
//使用lambda表达式创建线程
/*
auto mylbtd = []{
cout<< "子线程开始" <<endl;
//...子线程操作
cout << "子线程结束" << endl;
};
std::thead td(mylbtd);
td.join();
*/
cout << "主线程结束" <<endl;
return 0;
}
ta在主线程结束之后被立即释放,为什么线程中的类对象还能正常调用类相关函数?
原因在于ta被拷贝了一份到子线程中,因此主线程退出清理ta并不影响子线程中的ta,通过拷贝构造函数可以验证这一过程。程序运行的结果如下:
(三)线程传参
1. 传递临时对象做为线程参数
#include <iostream>
#include <thread>
using namespace std;
void myprint(const int & i, char *pmybuf){
cout << i <<endl;
cout << pmybuf <<endl;
}
int main(){
int val = 100;
int &qval = val;
char mybuf[] = "this is a string";
thread td(myprint, val, mybuf);
td.detach();
cout << "finished" <<endl;
return 0;
}
多运行几次结果如下图,这就是一个典型的问题,原因在于子线程使用的detach,主线程已经退出了但是子线程还在执行,主线程退出了就会释放指针指向的内容,因此不会将字符串打印出来。
上述代码中,传入引用没有问题,但是不推荐,因为会创建一个新的引用地址,并不是真正意义上的引用,第二个参数为指针一定会有问题,因为该指针所指向的内容可能在主线程结束时被释放。
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void myprint(const int & i, const string &pmybuf){
cout << i <<endl;
cout << pmybuf <<endl;
}
int main(){
int val = 100;
int &qval = val;
char mybuf[] = "this is a string";
thread td(myprint, val, mybuf); //这个mybuf可能会在主线程结束后再传入线程,是不安全的,解决方法是将其转为临时对象传入线程
//thread td(myprint, val, string(mybuf)); //安全的做法
td.detach();
cout << "finished" <<endl;
return 0;
}
总结:
- 若传递基本类型的参数建议直接使用值传递
- 传递类对象时,避免隐式类型转换,应该在创建线程时构建临时对象(会在主线程中构建完成),线程参数使用引用参数来接收实参(否则对象会创建3次:1次为构建临时对象,2次为临时对象传入触发拷贝对象函数,3次为线程中复制触发拷贝构造函数)
- 非特殊情况尽量使用join()
2. 线程id
每个线程都有唯一的id。代码中使用std::this_thread::get_id()获取线程id
3. 传入类对象、智能指针
-
在线程中传入类对象时,不论函数形参是不是引用,都会得到一个新的拷贝对象,此时修改该对象并不影响实参。如何解决这个问题?
使用std::ref()函数,同时使用join()可以使传入的参数始终为同一个对象。
-
智能指针unique_ptr在传入线程时,需要使用std::move()函数将参数传入,此时线程形参接收到的地址和原地址一致,但是主线程中的智能指针为空,需要注意的是必须使用join(),使用detach()可能导致主线程将智能指针所指对象释放而在线程中使用的情况,造成未知错误。
(四)多线程和数据共享
1. 创建并等待多个线程
#include <iostream>
#include <vector>
#include <thread>
using namespace std;
void myprint(int i){
cout << "Thread start ..." <<endl;
//....
cout << "Thread end ..." << endl;
return;
}
int main(){
vector<thread> myThreads; //使用容器存储线程
//创建10个线程
for(int i = 0; i<10; i++){
myThreads.push_back(thread(myprint, i));
}
//让主线程等待10个线程运行完成
for(auto iter = myThreads.begin();iter!=myThreads.end(); ++iter){
iter->join();
}
cout << "Main Thread end..." << endl;
return 0;
}
下图是程序运行的结果,产生这种结果的原因是多线程的并发问题,多个线程同时执行,此时cpu的时间片会不停的轮转,所以说线程执行和结束的顺序会产生一些不同,但是可以清楚的看到不管顺序有多乱,但是因为使用的是join(),因此主进程是在子进程执行完之后才结束的。
2. 数据共享
2.1. 只读数据
只读数据是安全稳定的,直接读就行。
2.2 有读有写
需要特殊的处理,避免程序崩溃。写的时候不能读,任意两个线程不能同时写,其他线程不能同时读。
2.3 共享数据代码
#include <iostream>
#include <vector>
#include <list>
#include <thread>
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
Msg.push_back(i); //语句1
}
}
//读取消息
void readMsg(){
int curMsg;
for(int i = 0; i < 10000; i++){
if(!Msg.empty()){
//读取消息,读完删除
curMsg = Msg.front(); //语句2
Msg.pop_front();
cout << "消息已读出" << curMsg << endl;
}else{
//消息暂时为空
}
}
}
private:
std::list<int> Msg; //消息变量
};
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
多运行几次就会出现这样的报错,原因在于写入线程还没有将数据写入,读取线程就开始都线程,并且将读到的内容删除,这样就会导致崩溃。
上述代码在执行的过程中有问题,原因在于语句1在执行插入操作的时候,语句2可能进行读和删除的操作,导致线程运行不稳定。解决方法:引入**互斥量(Mutex)**的概念。
(五)互斥量与死锁
1. 互斥量的基本概念
多个线程同时操作一个数据的时候,需要对数据进行保护,可以使用锁,让其中一个线程进行操作,其他线程处于等待状态。
互斥量可以理解成一把锁,多个线程尝试使用lock()函数对数据进行加锁,只有一个线程能锁定成功,其他线程会不断的尝试去锁数据,直到锁定成功。
互斥量使用需要小心,保护太多影响效率,保护不够会造成错误。
#include <iostream>
#include <vector>
#include <list>
#include <thread>
#include <mutex> //引入互斥量头文件
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
my_mutex.lock();
Msg.push_back(i);
my_mutex.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
my_mutex.lock(); //语句1
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
my_mutex.unlock(); //语句2
return true;
}
my_mutex.unlock();//语句3
return false;
}
private:
std::list<int> Msg; //消息变量
std::mutex my_mutex; //互斥量对象
};
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
这里将上述代码中的后面一句unlock注释掉,会产生下面的错误,因此lock和unlock必须是一一对应的,绝对不能单独使用。
2. 互斥量的用法
互斥量是一个对象。
2.1 lock和unlock
- lock()和unlock()必须成对使用,且只能出现一次,否则代码不稳定。
- 上述代码中,语句1中的lock()和后续语句2和3的unlock()成对出现,缺失任意一个unlock()都会导致程序崩溃。
2.2 std::lock_guard类模板
lock_guard的提出是为了防止程序员在使用lock()的时候忘记unlock()的情况,可以直接取代这两个函数,使用lock_guard之后不能再使用这两个函数。使用方式如下:
bool MsgLULProc(int &command) {
//my_mutex.lock(); //语句1
std::lock_guard<std::mutex> lgmutex(my_mutex); //使用lock_guard代替lock
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
//my_mutex.unlock(); //语句2 *使用lock_guard之后不需要自己手动释放锁
return true;
}
//my_mutex.unlock();//语句3 *使用lock_guard之后不需要自己手动释放锁
return false;
}
代码中语句1在lock_guard的构造函数中执行,mutex::lock(),在其析构的时候执行mutex::unlock(),由此保证了互斥量的正常使用。
lock_guard缺点是没有lock和unlock使用灵活,需要手动析构。可以使用{}包裹,达到提前析构的目的。见如下代码。
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
//在{}包裹内,lock_guard在{}结束时会自动析构,相当于unlock
{
std::lock_guard<std::mutex> lgmutex(my_mutex);
Msg.push_back(i);
}
}
return;
}
这里有一点需要额外注意:一旦使用了lock_guard之后,不能再使用lock和unlock。
3. 死锁
死锁是指两个(多个)线程相互等待对方数据的过程,死锁的产生会导致程序卡死,不解锁程序将永远无法进行下去。
3.1 死锁产生原因
举个例子:两个线程A和B,两个数据1和2。线程A在执行过程中,首先对资源1加锁,然后再去给资源2加锁,但是由于线程的切换,导致线程A没能给资源2加锁。线程切换到B后,线程B先对资源2加锁,然后再去给资源1加锁,由于资源1已经被线程A加锁,因此线程B无法加锁成功,当线程切换为A时,A也无法成功对资源2加锁,由此就造成了线程AB双方相互对一个已加锁资源的等待,死锁产生。
理论上认为死锁产生有以下四个必要条件,缺一不可:
- 互斥条件:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
- 不剥夺条件:进程在所获得的资源未释放前,不能被其他进程强行夺走,只能自己释放。
- 请求和保持条件:进程当前所拥有的资源在进程请求其他新资源时,由该进程继续占有。
- 循环等待条件:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。
3.2 死锁演示
通过代码的形式进行演示,需要两个线程和两个互斥量。
#include <iostream>
#include <vector>
#include <list>
#include <thread>
#include <mutex> //引入互斥量头文件
using namespace std;
class A {
public:
//插入消息,模拟消息不断产生
void insertMsg() {
for (int i = 0; i < 10000; i++) {
cout << "插入一条消息:" << i << endl;
my_mutex1.lock(); //语句1
my_mutex2.lock(); //语句2
Msg.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
MsgCom = MsgLULProc(i);
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
int curMsg;
my_mutex2.lock(); //语句3
my_mutex1.lock(); //语句4
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
private:
std::list<int> Msg; //消息变量
std::mutex my_mutex1; //互斥量对象1
std::mutex my_mutex2; //互斥量对象2
};
int main() {
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
语句1和语句2表示线程A先锁资源1,再锁资源2,语句3和语句4表示线程B线索资源2再锁资源1,具备死锁产生的条件。
程序运行到这里已然产生了死锁,不会再继续往下运行。
3.3 死锁的解决方案
保证上锁的顺序一致。
3.4 std::lock()
功能:锁住两个或两个以上的互斥量,解决因lock()顺序问题导致的死锁问题。
在时间使用过程中,只要有一个互斥量没锁住,就会进行等待,等所有互斥量都做锁住时,程序才继续进行。
要么多个互斥量都锁住,要么都没锁住,只要有一个没锁成功,会立即释放所有已经加锁的互斥量。代码如下:
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
std::lock(my_mutex1, my_mutex2);//顺序无所谓
Msg.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
该函数一次能锁定多个互斥量,小心使用,多个互斥量的时候建议逐个lock()和unlock()。
3.5 std::lock_guard的std::adopt_lock参数
std::adopt_lock是一个结构体对象,起一个标记作用就是表示这个互斥量已经lock(),不需要在std::lock_guard<std::mutex>构造函数里对mutex对象进行lock()了。使用这个参数配合std::lock()可以做到无需手动unlock()。代码如下:
void insertMsg(){
for(int i = 0; i < 10000; i++){
cout<< "插入一条消息:" << i << endl;
std::lock(my_mutex1, my_mutex2);//顺序无所谓
//加上adopt_lock参数可以使互斥量不再次进行lock()
std::lock_guard<std::mutex> lgmutex1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lgmutex2(my_mutex2, std::adopt_lock);
Msg.push_back(i);
}
}
(六)unique_lock的使用
6.1 作用描述
std::unique_lock可以完全取代std::lock_guard,在使用上更加灵活。
6.2 参数说明
- std::adopt_lock:标记作用,如果互斥量已经lock,则不需要再lock;
- std::try_to_lock:尝试去lock,如果没有锁定成功,会立即返回而不会阻塞,注意其之前不能先lock;
- std::defer_lock: 初始化一个未加锁的mutex,其之前也不能先lock,否则会报异常,在之后操作锁的时候需要手动给其加锁,比如try_lock()。
6.3 成员函数
- lock():给互斥量加锁;
- unlock:互斥量解锁;
- try_lock():尝试给互斥量加锁,如果拿不到锁,则返回false,不阻塞;
- release():释放互斥量的所有权,返回所管理的mutex对象指针,此后unique_lock和mutex不再有关系,如果,mutex处于加锁状态,则负责接管的对象需要负责解锁
6.4 unique_lock转移所有权的方式
- 使用std::move()函数进行转移
- 创建函数返回临时unique_lock对象
#include <iostream>
#include <vector>
#include <list>
#include <thread>
#include <mutex> //引入互斥量头文件
using namespace std;
class A {
public:
//插入消息,模拟消息不断产生
void insertMsg()
{
for (int i = 0; i < 10000; i++)
{
< lock_guard()给代码加锁演示代码
//std::unique_lock<std::mutex> ul(my_mutex);
//Msg.push_back(i);
//cout << "插入一条消息:" << i << endl;
< adopt和try_to_lock两个函数的解释
//my_mutex.lock();
//std::unique_lock<std::mutex> ul(my_mutex, std::adopt_lock); //std::adopt_lock标记已经加锁,前面需要lock
//std::unique_lock<std::mutex> ul(my_mutex, std::try_to_lock); //std::try_to_lock尝试加锁,前面不能先lock
< defer_lock()初始化未加锁的mutex
//std::unique_lock<std::mutex> ul(my_mutex, std::defer_lock); //std::defer_lock初始化一个未加锁的mutex,其之前也不能先lock,否则会报异常
//if (ul.try_lock()) { //判断是否拿到锁
// //拿到锁
// Msg.push_back(i);
//}
//else
//{
// //没有拿到锁
// cout << "写数据线程没有拿到锁" << endl;
//}
< release释放锁的所有权代码演示
//std::unique_lock<std::mutex> ul(my_mutex); //演示所有权释放
//Msg.push_back(i);
//std::mutex * p_m = ul.release(); //接管的互斥量指针需要手动释放已加锁的互斥量
//p_m->unlock();
< 转移锁代码演示
std::unique_lock<std::mutex> ul(my_mutex); //演示所有权转移,需要使用移动语义
std::unique_lock<std::mutex> ul2 = std::move(ul);
Msg.push_back(i);
ul2.unlock();
}
}
//读取消息
void readMsg() {
int MsgCom;
for (int i = 0; i < 10000; i++) {
if (MsgLULProc(MsgCom)) {
//读出消息了
cout << "消息已读出" << MsgCom << endl;
}
else {
//消息暂时为空
cout << "消息为空" << endl;
}
}
}
//加解锁代码
bool MsgLULProc(int &command) {
std::unique_lock<std::mutex> ul(my_mutex);
//延迟1s
std::chrono::milliseconds dura(1000);
std::this_thread::sleep_for(dura);
if (!Msg.empty()) {
//读取消息,读完删除
command = Msg.front();
Msg.pop_front();
return true;
}
return false;
}
private:
std::list<int> Msg; //消息变量
std::mutex my_mutex; //互斥量对象
};
int main() {
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
(七)单例设计模式与数据共享
7.1 单例设计模式
单例类:指的是在程序中该类的实例只存在一个,其实现通常需要满足以下三个条件:
- 构造函数私有化
- 唯一的私有静态类实例成员变量
- 静态方法返回类实例
实例代码7-1:
#include <iostream>
#include <vector>
#include <list>
#include <thread>
#include <mutex> //引入互斥量头文件
using namespace std;
class MyCAS{
private:
MyCAS(){}; //私有化构造函数,保证该类无法被new或者以MyCAS m方式生成实例
private:
static MyCAS * m_instance; //类实例
public:
static MyCAS * GetInstance(){ //返回类实例
if(m_instance == NULL){
m_instance = new MyCAS();
static GCclass gc;//用于回收上一句new产生的内存,在程序结束时会调用其析构函数
}
return m_instance;
}
//类中嵌套回收类,用于回收单例类实例,防止出现内存泄露
class GCclass{
~GCclass(){
if(MyCAS::m_instance){
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
};
//初始化单例类实例
MyCAS* MyCAS::m_instance = NULL;
int main(){
MyCAS *p_a = MyCAS::GetInstance(); //获取单例类对象,最好在使用多线程之前加载单例类实例
return 0;
}
7.2 数据共享
在多线程中,如果多个类同时创建单例类对象,需要进行互斥,因此引入互斥量进行加锁。在代码7-1中,加入互斥量并修改函数GetInstance():
std::mutex my_mutex; //引入互斥量
static MyCAS * GetInstance(){ //返回类实例
if(m_instance == NULL){//双重锁定,保证有一次实例化之后,不会再进行资源锁定
std::unique_lock<std::mutex> myul(my_mutex);
if(m_instance == NULL){
m_instance = new MyCAS();
static GCclass gc;//用于回收上一句new产生的内存,在程序结束时会调用其析构函数
}
}
return m_instance;
}
7.3 call_once()函数
该函数的功能是保证在多线程中,某一个函数只能被执行一次,可以解决7.2中GetInstance()函数被多次调用的问题。需要与标记std::once_flag配合使用。
在代码7-1中,进行如下修改:
std::once g_flag; //引入once_flag标记
//增加函数
static void CreateInstance(){
m_instance = new MyCAS();
static GCclass gc;
}
//返回类实例
static MyCAS * GetInstance(){
std::call_once(g_flag,CreateInstance);
return m_instance;
}
运行结果如下图所示:使用call_once保证只产生一个单例类
(八)条件变量及其成员函数
8.1 condition_variable类
条件变量可以使用通知的方式实现线程同步,其履行发送者或者接受者的角色。
实例代码8-1:
#include <condition_variable> //需要引入头文件
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;
class A{
public:
//插入消息,模拟消息不断产生
void insertMsg(){
for(int i = 0; i < 10000; i++){
std::unique_lock<std::mutex> myul<my_mutex>; //加锁
cout<< "插入一条消息:" << i << endl;
Msg.push_back(i);
myul.notify_one(); //语句1
}
}
//读取消息
void readMsg(){
while(true){
std::unique_lock<std::mutex> myul<my_mutex>; //加锁
my_cond.wait(myul,[this]{ //语句2
if(!Msg.empty())
return true;
return false;
});
}
}
//加解锁代码
bool MsgLULProc(int &command){
int curMsg;
my_mutex.lock(); //语句1
if(!Msg.empty()){
//读取消息,读完删除
curMsg = Msg.front();
Msg.pop_front();
cout << "消息已读出" << curMsg << endl;
my_mutex.unlock(); //语句2
return true;
}
my_mutex.unlock();//语句3
return false;
}
private:
std::list<int> Msg; //消息变量
std::mutex my_mutex; //互斥量对象
std::condition_variable my_cond; //条件变量
}
int main(){
A a;
//创建一个插入消息线程
std::thread insertTd(&A::insertMsg, &a); //这里要传入引用保证是同一个对象
//创建一个读取消息线程
std::thread readTd(&A::readMsg, &a); //这里要传入引用保证是同一个对象
insertTd.join();
readTd.join();
return 0;
}
8.2 wait()/notify_one()/notify_all()函数
语句2中使用wait()函数进行等待,第一个参数为unique_lock()类对象,第二个参数为lambda表达式,如果是true则直接返回,程序继续往下执行,如果为false,则将互斥量解锁,并阻塞到本行,直到有线程调用notify_one()成员函数将其唤醒(如语句2)。如果没有第二个参数,则默认为false。
**注意:**当wait()被唤醒后,会尝试重新拿锁,拿到则程序继续往下执行,notify_one()是唤醒一个处于wait状态的线程,如果有多个线程,则不确定会唤醒哪一个,而notify_all()是唤醒所有处于wait状态的线程。另外一点是,如果在notify的过程中,没有线程处于wait状态,则这个通知会丢失。
结语
本文转载自GitHub的一位大佬,本人做了一些增减。全文代码地址:https://github.com/wlonging/ThreadLearning,包含线程相关知识和一个线程池的项目。
代码都经过实际运行测验,如有问题,欢迎大家留言交流。