C++thread线程相关

声明:本文是在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;
}

总结:

  1. 若传递基本类型的参数建议直接使用值传递
  2. 传递类对象时,避免隐式类型转换,应该在创建线程时构建临时对象(会在主线程中构建完成),线程参数使用引用参数来接收实参(否则对象会创建3次:1次为构建临时对象,2次为临时对象传入触发拷贝对象函数,3次为线程中复制触发拷贝构造函数)
  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双方相互对一个已加锁资源的等待,死锁产生。

理论上认为死锁产生有以下四个必要条件,缺一不可:

  1. 互斥条件:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
  2. 不剥夺条件:进程在所获得的资源未释放前,不能被其他进程强行夺走,只能自己释放。
  3. 请求和保持条件:进程当前所拥有的资源在进程请求其他新资源时,由该进程继续占有。
  4. 循环等待条件:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。
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,包含线程相关知识和一个线程池的项目。

代码都经过实际运行测验,如有问题,欢迎大家留言交流。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值