Winndows\C++并发与多线程

并发与多线程

一、并发、进程、线程的基本概念

1、并发:

​ 两个或者更多独立的任务同时发生:一个程序同时执行多个独立的任务;

​ 以往的计算机是假的并发,采用的是时分复用;这种切换是需要时间开销的

​ 硬件的发展出现了多处理器计算机,能够真正的并行执行多个任务。

2、进程

​ 进程就是一个可执行程序运行起来

3、线程

​ 每个进程都有一个主线程,一个进程只有一个主线程,当你执行可执行程序,这个主线程就默默启动,实际上是进程中的主线程来执行main函数中的代码。

​ 每创建其他线程,就可以在同一个时刻多干一个不同的事。

线程并不是越多越好,每个线程都需要一个独立的堆栈空间,线程之间的切换需要保存很多中间状态,耗费很多本该属于程序运行的时间。

  • 线程是用来执行代码的;
  • 线程理解为一条代码的执行通路
并发的实现
  • 通过多个进程实现并发
  • 在一个进程中通过多个线程实现并发
1、多进程并发
2、多线程并法
  • 一个进程中的所有线程共享地址空间(共享内存),全局变量、指针、引用都可以在线程之间传递;

  • 共享内存带来新问题,多线程导致数据一致性问题;

3、线程的优点
  • 线程启动资源更快,更轻量级;系统开销资源更少,执行速度更快

  • 使用有一定难度,小心处理数据一致性问题;

4、C++11标准多线程跨平台

二、线程启动、结束

1、创建线程、join、detach
#include <Thread>
void myprintf(){
	std::cout << "线程开始执行" << std::endl;
	std::cout << "线程执行完毕" << std::endl;
}
int main()
{
	/*主线程从main函数开始执行,子线程也需要从一个函数开始和结束
	主线程执行完毕,代表进程执行完毕;如果其他子线程还没有执行完毕,也会被操作系统强行终		止;这是不安全稳定的.
	
		1、thread
		2、join();//阻塞主线程
		3、detach();//将子线程和主线程分离,一旦detach之后,这个子线程就被C++运行时刻接					管,子线程执行由运行时库负责清理该线程相关资源,失去我们的控制;
			一旦detach,线程就不能再join;
		4、为什么引入detach:创建了很多子线程,让主线程逐个等待子线程,这种编程不太好
	*/
	std::thread mytobj(myprintf);//创建线程并启动,以myprintf函数为入口
	mytobj.join();//阻塞主线程,让主线程等待子线程执行完毕再继续
	if (mytobj.joinable()){//判断是否可以joinable
		std::cout << "mytobj能join" << std::endl;
		mytobj.join();
	}
	else
		std::cout << "mytobj不能join" << std::endl;
	//mytobj.detach();
	std::cout << "I love China" << std::endl;
	return 0;
}
2、用类对象创建

在这里插入图片描述

​ 上图中,对象a是在主线程中,一旦调用了detach(),那主线程结束时,子线程的对象还在,因为子线程中是复制的对象。

3、用lambda表达式
auto mylambda = []{
	std::cout << "我的线程开始创建" << std::endl;
	std::cout << "我的线程执行完毕" << std::endl;
};
int main()
{

	std::thread mytobj(mylambda);//lambda表达式
	mytobj.join();//等待子线程执行结束
	system("pause");
	return 0;
}
4、类对象引用问题

用类对象的一个问题:

class TA{
public:
	int m_i;
	TA(int &i):m_i(i){}//构造函数
	void operator()(){//不能带参数
		std::cout << "线程开始执行" << std::endl;
		std::cout << "线程执行完毕" << std::endl;
	}
};
int main()
{
	int myi=6;
	TA a(myi);
	std::thread mytobj(a);//a:可调用对象
	mytobj.detach();//交给C++运行时库
	system("pause");
	return 0;
}

​ 上述代码中,参数myi是在主线程中在引用传给类对象,且子线程detach( )在后台运行,主线程结束后,子线程参数会出现问题。但对象a是拷贝到子线程的,所以对象不会有问题。

三、线程传参详解

1、传递临时对象作为线程参数
void myprint(const int &i,char *buf){
	std::cout << i << std::endl;//i并不是myi的引用,而是值传递
	std::cout << buf << std::endl;//buf是mybuf相同地址,不能传指针
}
int main()
{
	int myi = 9;
	int &vmyi = myi;
	char mybuf[] = "this is a test!";
	std::thread mytobj(myprint,myi,mybuf);
	mytobj.detach();
	system("pause");
	return 0;
}
*-----------------**-----------------**-----------------**-----------------*
void myprint(const int &i,string &buf){
	std::cout << i << std::endl;//i并不是myi的引用,而是值传递
	std::cout << buf << std::endl;//buf是拷贝
}
char mybuf[] = "this is a test!";
std::thread mytobj(myprint,myi,mybuf);//隐式转换
std::thread mytobj(myprint,myi,string(mybuf));//直接转换
mytob.detach();

​ 事实上存在情况,mybuf已经被回收,系统才将参数隐式转换为string,因此一般采用直接转换的方式来进行。

void myprint(const int &i,const TA &buf){
	std::cout << i << std::endl;//i并不是myi的引用,而是值传递
	std::cout << buf.m_i << std::endl;//buf不是mybuf相同地址
}
class TA{
public:
	int m_i;
	TA(int &i):m_i(i){
		std::cout << "线程开始执行" << std::endl;
		std::cout << "线程执行完毕" << std::endl;
	}
};
int main()
{
	int myi = 9;
	int vmyi = 12;
	std::thread mytobj(myprint,myi,vmyi);//可以完成隐式转换
	std::thread mytobj(myprint,myi,A(vmyi));//直接转换
	mytobj.detach();
	system("pause");
	return 0;
}
  • 若传递int这种简单类型,建议值传递
  • 如果传递类对象,避免隐式类型转换
  • 建议不适用detach
2、线程ID

​ 每个线程实际上都有一个线程ID,可以用std::this_thread::get_id()来获取;

通过get_id()可以发现,隐式类型转换的拷贝构造函数是在子线程中进行的。所以进行显示类型转换,是在主线程中进行的。

void myprint(const int &i,const A &buf){
	std::cout << "子线程ID:" << std::this_thread::get_id();
}
class TA{
public:
	int m_i;
	TA(int &i):m_i(i){
	std::cout << "构造函数ID:" << std::this_thread::get_id();
	}
};
int main()
{
	int myi = 9;
	int vmyi = 12;
	std::cout << "主线程ID:" << std::this_thread::get_id();
	std::thread mytobj(myprint,myi,vmyi);//可以完成隐式转换
	//std::thread mytobj(myprint,myi,A(vmyi));//直接转换
	mytobj.detach();
	system("pause");
	return 0;
}
  • 在主线程中传参给子线程,无论子线程参数是否是引用,主线程都会产生一个拷贝参数给子线程

  • 此时若子线程参数为引用,则直接使用拷贝给子线程的参数,否则还会产生一个拷贝构造;

3、std::ref

​ 由于开辟子线程会强制产生拷贝构造函数 ,此时为了使用主线程中的引用,使用ref函数,进行强制类型转换;

int my_num=9;

std::thread mytobj(myprint,std::ref(my_num));//强制类型转换

4、传智能指针
#include<memory>

void myprint(unique_ptr<int> pzn){

}
unique_ptr<int> myp(new int(100));//建立一个指向整形的智能指针myp
std::thread mytobj(myprint,std::move(myp));

​ 需要使用move函数进行智能指针的传递,因为unique_ptr是排他性唯一指针,而创建子线程会使用拷贝构造,这是不允许的。

成员函数指针做线程函数
class TA{
public:
	int m_i;
	TA(int &i):m_i(i){
	std::cout << "构造函数ID:" << std::this_thread::get_id();
	}
	void thread_work(int num){
		std::cout << "成员函数做函数入口:" << std::this_thread::get_id();
	}
};
int main()
{
	TA myobj(10);
	std::thread mytobj(&TA::thread_work,myobj,10);/取地址符
	mytobj.join();
	system("pause");
	return 0;
}

​ 使用成员函数传参时,必须将创建的对象传进去才可以。

四、创建多个线程

1、创建多个线程
void myprint(int num){
	cout << "线程kaishi执行" << endl;
	cout << "线程ID::" << this_thread::get_id() << endl;
	cout << "线程结束执行" << endl;
}
int main()
{
	vector<thread> mythread;
	for (int i = 0; i < 10; ++i){
		mythread.push_back(thread(myprint, i));
		//mythread[i].join();//这种join是完成一个线程在开始创建下一个线程
	}
	for (auto i = mythread.begin(); i != mythread.end(); ++i)
		i->join();//这种join使得内部调度是乱的
	system("pause");
	return 0;
}

​ 使用vector容器使得对多个线程的管理变得清晰

2、数据共享
①只读数据
vector<int> num = { 1, 2, 3 };//全局变量
void myprint(){

	cout << "线程ID::" << this_thread::get_id() << endl;
	cout << num[0] << num[1] << num[2]<<endl;
}
int main()
{
	vector<thread> mythread;
	for (int i = 0; i < 10; ++i){
		mythread.push_back(thread(myprint));
		//mythread[i].join();
	}
	for (auto i = mythread.begin(); i != mythread.end(); ++i)
		i->join();
	system("pause");
	return 0;
}
  • 全局变量是可以共享的,以上是只读的数据,没有问题
②多线程既读又写

​ 2个线程写,8个线程读,如果代码没有特殊处理肯定会崩溃,最简单的处理办法:
读的时候不能写,写的时候不能读,2个线程不能同时写,8个线程不能同时读。

**因为:**容器是一个复杂的东西,写需要多步,读和写的东西可能会导致内存错乱。类似于数据库的锁的机制。

案例

​ 网络服务器:一个服务器用来收集玩家命令放入任务队列,另一个服务器取任务队列执行命令。

#include <iostream>
#include <Thread>
#include<list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数的方法来写方法
class A{
public:
	//把收到的命令入到任务队列
	void inMsgQue(){
		for (int i = 0; i < 100000; ++i){
			cout << "inMsg插入一个元素" << 1 << endl;
			my_mut.lock();
			msgRecvQueue.push_back(i);//收消息
			my_mut.unlock();
		}
	}
	//从任务队列取任务
	void outMsgQue(){
		for (int i = 0; i < 100000; ++i){
			my_mut.lock();//
			if (!msgRecvQueue.empty()){
				int command = msgRecvQueue.front();
				msgRecvQueue.pop_front();
				my_mut.unlock();//这是在if内的unlock
				if (command % 2 == 0){
					cout << "买" << endl;
				}
				else{
					cout << "卖" << endl;
				}
				continue;//这个也需要,否则有执行两次unlock的危险;
			}
			my_mut.unlock();//这是在if外的unlock
		}
	}
private:
	list<int>msgRecvQueue;//任务队列
	mutex my_mut;
};
int main()
{
	//引入互斥量mutex
	A myobj;
	thread myout(&A::outMsgQue, &myobj);//第二个参数是地址,才能保证用的是同一个对象
	thread myin(&A::inMsgQue, &myobj);//否则拷贝两次使用的是不同的
	myout.join();
	myin.join();
	system("pause");
	return 0;
}
③互斥量mutex

​ 互斥量是个类对象,理解为一把锁,多个线程尝试用lock()成员函数加锁,只有一个线程可以锁成功,成功则返回,如果没成功那么流程卡在lock()这里不断地尝试去锁。

lock,unlock()执行步骤:先lock(),操作共享数据后,再unlock(),

④std::lock_guard

​ 为了防止大家unlock, C++引入std::lock_guard的类模板直接替换lock和unlock;

调用方法:

std::lock_guard<std::mutex> mylock(my_mut);//这是创建lockguard类对象;

​ 相当于在创建lock_guard类对象和析构函数时分别调用lock和unlock;

3、死锁

比如有两把锁:金Lock和银Lock,金锁用来抢资源M,银锁用来抢资源N,两个线程A、B:

​ * 线程A锁金锁,lock成功后,然后尝试去锁银锁;

​ * 出现上下文切换

​ * 线程A锁银锁,lock成功后,然后尝试去锁金锁;

此时出现死锁

①死锁的解决方案

只要保证两个互斥量的锁定资源的顺序一致就不会出现死锁的情况

②std::lock函数模板

​ 一次锁住多个,至少两个,一个不行,这样不会出现死锁的情况。

​ 要么都锁住,要么都锁不住。

std::lock(my_mutex1,my_mutex2);
需要手动unlock();
my_mutex1.unlock();
my_mutex2.unlock();
③ lock和:lock_guard

在这里插入图片描述

adopt_lock的作用是因为已经lock,否则lock_guard会报错。

五、unique_lock

1、取代lock_guard( )

unique_lock是一个类模板,一般使用lock_guard即可。unique_lock比lock_guard更灵活,但代价更高。

1、std::lock_guard<std::mutex> mylock(my_mut);

2、std::unique_lock<std::mutex> mylock(my_mut);

3、std::unique_lock<std::mutex> mylock(my_mut,adopt_lock);

    4、std::unique_lock<std::mutex> try_ml(my_mut,try_to_lock);
        if(try_ml.owns_lock()){
            ;
        }

其他参数:

  • std::adopt_lock参数:通知unique_lock 互斥量已经被lock,不再需要lock;

  • try_to_lock:尝试锁mutex, 没成功也不会阻塞,前提是不能先去lock;与owns_lock结合使用;

  • std::defer_lock:初始化一个没有加锁的mutex,前提是不能自己先lock

    std::unique_lock<std::mutex> sub_gu(my_mut,defer_lock);//初始化
    sub_gu.lock();
    
2、成员函数
1)lock()函数
std::unique_lock<std::mutex> sub_gu(my_mut,defer_lock);//初始化
sub_gu.lock();//不用自己unlock()
2)unlock()函数

希望处理锁更灵活些,采用手动解锁unlock()

3)try_lock()
std::unique_lock<std::mutex> sub_gu(my_mut,defer_lock);//声明&初始化
if(sub_gu.try_lock()==true){
	;
}//与truy_to_lock类似
5)release()函数

返回它所管理的mutex对象指针,并释放所有权;

区分unlock和release的区别。

std::unique_lock<std::mutex> sub_gu(my_mut,defer_lock);//初始化
std::mutex * pmut=sub_gu.release();//释放所有权
pmut->unlock();//自己unlock;
3、所有权的传递
1、std::unique_lock<std::mutex> sub_gu1(my_mut1);//拥有所有权

此时sub_gu1拥有my_mut1的所有权,可以进行所有权的转移,但不能复制

2、std::unique_lock<std::mutex> sub_gu2(std::move(sub_gu1));//所有权转移了

此时此时sub_gu2拥有my_mut1的所有权,sub_gu1指向空

六、设计模式

1、大概谈
  • ‘’设计模式‘’就是代码的写法,维护可能很灵活,但是别人接管很痛苦;

  • 设计模式尤其独特的优点,但不要乱用

2、单例设计模式

单例:整个项目中,有某些特殊的类,该类的对象,只能创建一个。

1)单例类
class CAS{
private:
	static CAS* m_instance;
	CAS() {}//私有化构造函数
public:
	static CAS* Getinstance(){
		if (m_instance == NULL)
			m_instance = new CAS();
		return m_instance;
	}
	class Chuishou{//类中套类,用来释放对象
	public:
		~Chuishou(){
			if (CAS::m_instance){
				delete CAS::m_instance;
				CAS::m_instance = NULL;
			}	
		}
	};
};
CAS* CAS::m_instance = NULL;//类外初始化静态变量

int main()
{
	//不能使用默认构造函数
	CAS *p_a = CAS::Getinstance();
	system("pause");
	return 0;
}
2)共享数据分析

需要在多个子线程中创建CAS单例类

void my_thread(){
	std::cout<<"子线程开始"<<endl;
	CAS a=Getinstance();
}
int main()
{
	thread tobj1(my_thread);
	thread tobj2(my_thread);
	//不能使用默认构造函数
	CAS *p_a = CAS::Getinstance();
	system("pause");
	return 0;
}

面临问题:多个线程可能在判断m_instance是否为空的地方出现错读,因此需要加锁解决此问题,但加锁将导致代码执行效率降低,

static CAS* Getinstance(){
	if (m_instance == NULL){//双重检查
		unique_lock<mutex> my_mutex(my_mut);
		if(m_instace==NULL){
			m_instance = new CAS();
			return m_instance;
		}
	}
}
3、call_once()函数

call_once的第二个参数是一个函数名a(),保证函数a()只被调用一次。他是通过一个标志位once_flag进行处理的。

std::once_flag g_flag;//once_flag也是一个结构体
class CAS{
private:
	static CAS *m_instance;
	CAS() {}//私有化构造函数
public:
	static void CreateInstance(){
		m_instance = new CAS();
		static Chuishou c1;//自动释放类对象
	}
	static CAS* Getinstance(){
		std::call_once(g_flag, CreateInstance);
		return m_instance;
	}

	class Chuishou{//类中套类,用来释放对象
	public:
		~Chuishou(){
			if (CAS::m_instance){
				delete CAS::m_instance;
				CAS::m_instance = NULL;
			}	
		}
	};
};
CAS *CAS::m_instance = NULL;//类外初始化静态变量

七、条件变量

1、std::condition_variable

线程A:等待一个条件满足

线程B:专门往队列消息扔消息

​ std::condition_variable是一个类,是一个和条件相关的类,这个类需要和互斥量来配合工作,用的时候要生成这个类的对象。

2、wait()和notify_one()

wait()、condition_variable和notify_one()需要结合使用:

  • wait用来等待一个true值,如果第二个参数lambda表达式返回true,那么wait直接返回,进行执行下面的代码;
  • 如果第二个参数lambda表达式返回false,那么wait解锁互斥量,阻塞到本行;
  • 那么阻塞到另外一个线程调用notify_one()成员函数为止
  • 如果wait没有第二个参数,那么类似于自动返回false
  • wait唤醒后将继续尝试加锁,再执行到wait后继续获取lambda表达式的返回值
#include <condition_variable>
class A{
public:
	//把收到的命令入到任务队列
	void inMsgQue(){
		for (int i = 0; i < 100000; ++i){
			cout << "inMsg插入一个元素" << 1 << endl;
			unique_lock<mutex> mmn(my_mut);
			msgRecvQueue.push_back(i);//收消息
			m_condvar.notify_one();
		}
	}
	//从任务队列取任务
	void outMsgQue(){
		for (int i = 0; i < 100000; ++i){
			unique_lock<mutex> mm(my_mut);
			m_condvar.wait(mm, [this]{
				if (msgRecvQueue.empty())
					return false;//如果队列为空,则释放锁,让写进程抢锁,然后其notify();
				return true;
			});
			//能执行到这里说明,表达式返回true,锁还是锁着的
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			mm.unlock();//手动提前解锁,mm再重新抢锁,而不是再直接判别队列是否为空
			if (command % 2 == 0)
				cout << "买" << endl;
			else
				cout << "卖" << endl;
		}
	}
private:
	list<int>msgRecvQueue;//任务队列
	mutex my_mut;
	condition_variable m_condvar;//
};
int main()
{
	//引入互斥量mutex
	A myobj;
	thread myout(&A::outMsgQue, &myobj);//第二个参数是地址,才能保证用的是同一个对象
	thread myin(&A::inMsgQue, &myobj);//否则拷贝两次使用的是不同的
	myout.join();
	myin.join();
}
3、深入思考
4、notify_all()

notify_one只能唤醒一个在wait状态的其他线程。

condition_variable m_condvar;
m_condvar.notify_all();//唤醒所有其他线程

但是在上面这个例程中,notify_onenotify_all的效果是一样的。

八、创建后台任务

1、返回值的线程

std::asyncstd::future创建后台任务并返回值。

std::async是一个函数模板,用来自动启动一个线程并执行对应的入口函数,并将返回值包含在std::future类模板对象中,利用future的成员函数get一直等待线程执行结束得到返回值后可以得到结果。

#include<future>
cout<<"aaaa"<<endl;
	/*
	...
	*/
	return 5;
}
std::future<int> result=std::async(mythread);
cout<<result.get();//得到5
2、async的参数

通过向async()传递一个参数,改参数类型是std::launch,来达到目的:

参数:

  • std::launch::deferred:表示线程被延迟到std::future()的wait()或者get()函数才执行,而且并没有创建新线程,是在主线程调用的入口函数;

    std::future<int> result=std::async(std::launch::deferred,mythread)

  • std::launch:async: 在调用async函数的时候就开始创建线程;

    std::future<int> result=std::async(std::launch::async,mythread)

  • std::packaged_task:是个类模板,模板参数是各种可调用对象。

3、packaged_task

相当于把线程封装起来:

函数入口:

int mythread(double num){
	cout<<"子线程开始"<<endl;
	cout<<std::this_thread::get_id()<<endl;
	num+=6;
	return num;
}

函数封装:

std::packaged_task<int(double)> mypt(mythread);

执行:

mypt(100);
std::future result=mypt.get_future();
result.get();
4、promise

我们能够在某个线程中给它赋值,然后在另外线程中把这个值取出来。

void mythread(std::promise<int> &num,int calc){
	calc++;
	calc--;
	/*
	...
	*/
	int result=calc;
	num.set_value(result);//将结果保存到了num中
}
int main(){
	std::promise<int> myprom;//声明一个模板类对象,保存的值是int类型
	std::thread t1(mythread,std::ref(myprom),18);
	t1.join();
	//获取结果值
	std::future<int> fu1=myprom.get_future();//promise<int> 
	int result=fu1.get();
}

我们这里的其他线程是用主线程演示的。

九、future的其他函数

1、wait_for()

在这里插入图片描述

2、shared_future

future只能get一次,因为他是移动参数,而shared_future的get()是复制数据;

std::future<int> result=mypt.get_future();
std::shared_future<int> res(std::move(result));//执行完毕后result里面也是空的
std::shared_future<int> res(result.share());//执行完毕后result里面也是空的
auto k_ij=res.get();
k_ij=res.get();//可以重复get()
3、原子操作
1)概念

​ 有两个线程对同一个变量进行操作时,即使是简单的赋值和读,由于汇编是分多部进行的,也会出现出错。原子操作是可以不使用锁的技术也能完成并发机制。不会被打断的程序片段,效率更高。

2)用法范例

std::automic<>是一个类模板

std::automic<int> g_num=0;
void mycount(){
	for(int i=0;i<100000;++i){
		++g_num;//原子操作
	}
}
thread t1(mycount);
thread t2(mycount);

此时对数的累加不会被的打断最终结果必定是200000.

3)心得

_future();//promise
int result=fu1.get();
}


我们这里的**其他线程**是用**主线程**演示的。

### 九、future的其他函数

#### 1、wait_for()

[外链图片转存中...(img-f71SJFGs-1596597081701)]

#### 2、shared_future

future只能get一次,因为他是移动参数,而shared_future的get()是复制数据;

std::future result=mypt.get_future();
std::shared_future res(std::move(result));//执行完毕后result里面也是空的
std::shared_future res(result.share());//执行完毕后result里面也是空的
auto k_ij=res.get();
k_ij=res.get();//可以重复get()


#### 3、原子操作

##### 1)概念

​			有两个线程对同一个变量进行操作时,即使是简单的赋值和读,由于汇编是分多部进行的,也会出现出错。**原子操作**是可以不使用锁的技术也能完成并发机制。不会被打断的程序片段,效率更高。

  ##### 2)用法范例

`std::automic<>`是一个类模板

```;
std::automic<int> g_num=0;
void mycount(){
	for(int i=0;i<100000;++i){
		++g_num;//原子操作
	}
}
thread t1(mycount);
thread t2(mycount);

此时对数的累加不会被的打断最终结果必定是200000.

3)心得

一般用于计数,比如统计发送数据包

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值