C++入门(智能指针和并发)

文章目录

  • 智能指针
    • 直接内存管理(new/delete)
            • new/delete
            • operator new() operator delete()(一般不直接用)
            • new如何分配内存
            • 申请和释放一个数组
            • 配对使用
        • 智能指针
            • shared_ptr
            • weak_ptr
        • shared_ptr使用场景、陷阱、性能说明、尺寸
        • unique_ptr
            • make_unique函数
        • 返回unique_ptr、删除器、尺寸
        • 总结(重要)
  • 并发与多线程
    • 概念
        • 并发
        • 新标准线程库
    • 线程启动、结束、创建线程方法、join、detach
        • 其他创建线程的手法
    • 线程传参详解、detach()大坑、成员函数指针做线程函数
        • 传递临时对象作为线程参数
        • 线程id概念:
        • 传递类对象作为线程参数
        • 用成员函数指针做线程函数
    • 创建多个线程,数据共享问题分析
        • 创建等待多个线程
        • 数组共享问题分析
        • 数据共享保护案例
    • 互斥量概念、用法、死锁演示及解决
        • 互斥量(mutex)
        • 死锁
    • unique_lock
        • unqiue_lock取代lock_guard
        • unique_lock的第二个参数
        • unique_lock的成员函数
        • unique_lock所有权传递
    • 单例设计模式共享数据分析、解决
        • 设计模式
        • 单例设计模式
        • 单例设计模式
        • 共享数据问题分析
        • std::call_once()
    • 条件变量condition_variable、wait()、notify_one()
    • async、future、packaged_task、promise
        • std::async、std::future创建后台任务并返回值
        • std::package_task
        • std::promise
    • future其他成员函数、shared_future、atomic
        • 其他成员函数
        • std::shared_future
        • 原子操作std::automic
    • std::atomic、std::async
        • std::atomic
        • std::async参数
    • widows临界区、其他互斥量
        • windows临界区
        • 多次进入临界区实验
        • 自动析构技术
        • 递归的独占互斥量recursive_mutex
        • 带超时的std::timed_mutex和std::recursive_timed_mutex
    • 补充知识、线程池、数量
        • 虚假唤醒
        • atomic
        • 线程池
        • 线程数量问题
        • 多线程总结

智能指针

直接内存管理(new/delete)

new分配方式称为动态分配(分配在堆上):直接内存管理(new.delete)

void func()
{
	int i;
	i = 5;//临时对象,系统在栈上进行分配,当函数结束内存被回收

	static int j;//局部静态对象,在静态区域分配,执行到此语句时才会分配内存,函数执行完毕也不会释放内存
}

class A

A a;//编译器创建a对象,在main结束后释放

初始化

int *point = new int;//动态分配,初值未定义,为随机数
int *point = new int(100);//用()给初值

string *mystr = new string;//空字符串,说明调用了string的默认构造函数
string *mystr2 = new string(5,'a');//生成一个字符串,‘aaaaa’

vecctor<int> *pointv = new vector<int>{1,2,3,4,5};//new容器对象,里面有五个元素

//值初始化,加一个()
string *mystr3 = new string();//也是空字符串
int *pointv = new int();//0值
//对类来说,值初始化没有区别
A *pa = new A;//新建对象
A *pa = new A();

new对象的时候,能值初始化就值初始化,防止随机值,没有坏处

auto可以和new配合使用

auto mystr3 = new auto (mystr2);//用2初始化3,即mystr3 = string**,是指针的指针

const对象动态分配

const int *pointci = new const int(200);

new和delete说明:
成对使用,delete的作用是回收用new分配的内存。不是new的,不能delete;
delete只能一次,不能多次,delete后内存就不能使用;
指向同一块内存的也不能重复释放;

MFC微软公司的一个基础程序框架(MFC基础类库),可以生成一个带窗口的程序框架,简化了很多界面开发工作。可以用来发现内存泄漏问题。

new/delete

是个关键字/运算符,不是函数,类似sizeof;
new和malloc的最主要区别:new具备对堆上所分配内存进行初始化/释放的能力,可以调用构造函数

//类A
A *pa =new A();//new调用构造函数
delete pa;//delete调用析构函数
operator new() operator delete()(一般不直接用)

operator new()是函数,new在分配内存时就是通过operator new()来分配内存,delete在释放内存就是通过operator delete()来释放内存

int *pi = new int;
delete pi;
//
void *myorgpoint = operator new(100);
new如何分配内存
int *p = new int;//4字节,有记录机制记录分配的内存大小
delete p;
申请和释放一个数组

注意格式

int *p = new int[2];//8个字节
delete[]p;//注意数组释放的格式 

A a;
int ilen = sizeof(a);//空类是一个字节
A *pa = new A();//类的成员函数不占用类的字节,但是成员函数会占用类的字节
A *pa = new A[2]();//两个对象的数组,占六个字节,多出来的四个字节用来保存数组元素的个数2,但只针对是类类型
delete pa;//错误,对于类类型构成的数组,必须加[]
delete[]pa;

配对使用

内置类型比如int并没有调用构造函数,所以没有多分配4个字节;
对于int,new[], delete p,delete[]p效果一样;
如果一个对象,使用new[]分配内存,却用单独的delete(而不是delete[])释放内存,那么需要满足的类型时内置类型或者无析构函数的类类型;
[]也要配对使用

智能指针

int *p = new int();
int *q = p;
int *r = p;

pqr都指向同一段内存,只有pqr都不再使用了的时候,才能释放这段内存
p裸指针:直接用new返回的指针,需要全程维护,容易出错
指针指针解决裸指针带来的各种问题:对裸指针进行包装,最突出的是能自动释放所指向的对象内存,不用担心忘记释放。使用智能指针更便于编写和调试。
四种智能指针:auto_ptr(98,弃用), unique_ptr(11,独占式指针), shared_ptr(11,共享式指针), weak_ptr(11,辅助shared_ptr指针);
这三种指针指针都是类模板,vector,可以将new获得地址赋给它们;
智能指针本质就是不用手动delete

shared_ptr

共享所有权,不是被一个shared_ptr拥有,而是多个之间相互协作;
工作原理:引用计数,每个shared_ptr的拷贝都指向相同的内存,所以只有最后一个指向该内存的指针不需要再指向该对象时,这个shared_ptr才会去析构所指向的对象;
最后一个指向该内存的shared_ptr在以下情况下会释放该对象:
a)这个shared_ptr被析构的时候;
b)这个shared_ptr指向其他的对象的时;
类模板,用<>,就是指针可以指向的类型,再跟智能指针名
1)常规初始化

shared_ptr<int>pi(new int(100));//pi指向一个值为100的int数据

shared_ptr<int>makes(int value)//函数
{
	return shared_ptr<int>(new int(value))
} 
shared_ptr<int>pi3 = makes(130);

2)make_shared函数
标准库里的函数模板,安全高效的分配和使用shared_ptr;
能在动态内存(堆)中分配病初始化一个对象,然后返回指向此对象的shared_ptr

shared_ptr<int>p2=male_shared<int>(100);//这个shared_ptr返回一个值为100的整型内存,类似int *p = new int (100)
shared_ptr<string>p2 = make_shared<string>(5,'abc');//
shared_ptr<int>p4 = make_shared<int>();//p4指向一个int,值初始化
p4 = make_shared<int>(400);//p4指向新的int,里面保存的时400,首先释放刚才为0的内存,然后指向400的内存(智能)

auto p5 = make_shared<string>(5,'a');//

3)shared_ptr引用计数的增加和减少
共享式,引用计数,每一个shared_ptr的拷贝都指向相同的内存,只有最后一个指向该对象的shared_ptr指针不需要再指向该对象的时候,这个shared_ptr才会析构所指向的对象。

引用计数的增加:
每个shared_ptr都会记录有多少其他的shared_ptr指向相同的对象;

auto p6 = make_shared<int>(100);//此对象目前有一个引用者
auto p7(p6);//智能指针定义时的初始化,p6和p7指向了相同的对象,此对象目前有两个引用者

用p6初始化p7,引用计数增加1;
把智能指针当做实参往函数里传,引用计数增加1;(实参也指向该内容)
但是给函数传递引用的话就不会增加;
作为函数的返回值,当用指针来接时,引用计数加1,否则不加;

有指针指向新对象的时候,计数减一;
局部的shared_ptr离开作用域,计数减一;(退出函数)
当一个shared_ptr引用计数变成0,会自动释放自己所管理的对象;

2)shared_ptr常用操作
use_count():返回多少个智能指针指向该对象,主要用于调试;

shared_ptr<int> myp(new int(100));
int icout = myp.use_count();//1
shared_ptr<int> myp2(myp);
icout = myp.use_cout();//2
shared_ptr<int>myp3;
myp3 = myp2;
icout = myp3.use_count();//3
icout = myp.use_cout();//3

unique():是否该智能指针独占该对象,也就是若计数为1,则返回True,否则相反

shared_ptr<int>myp(new int (100));
//shared_ptr myp2(myp);
if (myp.unique())
{
	cout<< "OK" << endl;
}

reset():恢复/重置
不带参数时:若pi时唯一指向该对象的指针,则释放pi指向的对象,并将pi置空;若pi不是唯一指向该对象的指针,那么不释放pi所指向的对象,但指向该对象的引用计数会减少1,同时将pi置空。

shared_ptr<int>pi(new int(100));
pi.reset():
if (pi == nullptr )
{
	cout << "pi置空"<< endl;
}

带参数时:若pi时唯一指向该对象的指针,则释放pi指向的对象,让pi指向该对象;若pi不是唯一指向该对象的指针,则不释放该对象,但计数减一,同时让pi指向该对象。

shared_ptr<int>pi(new int(100));
pi.reset(new int(1));//释放原内存,指向该内存
if(pi.unique())
{
	cout << "unique" <<endl;
}

空指针

shared_ptr<int>p;
p.reset(new_int(1));//释放p指向的对象,因为为空所以让p直接指向新对象

*解引用:获得p指向的对象

shared_ptr<int>pi(new int(100));
cout << *pi << endl;//打印出100

get()
考虑到有些函数的参数需要的是一个内置指针而不是智能指针;
p.get():返回p中保存的指针(裸指针)

shared_ptr<int>myp(new int(100));
int *p = myp.get();
*p = 43;

swap()交换两个智能指针指向的对象(不常用)

=nullptr
将所指向的对象引用计数减一,若引用计数为0,则释放对象;
将智能指针置空

shared_ptr<string>psl(new string("abc"));
psl = nullptr;

智能指针名字作为判断条件

shared_ptr<string>psl(new string("abc"));
if(psl)
{
	cout << "psl指向一个对象" <<endl;
}

指定删除器以及数组问题
指定删除器:一定时机帮我们删除指向的对象;delete将delete运算符作为默认的资源析构方式。也可以指定自己的删除器取代系统默认的删除器,当智能指针需要删除所指向的对象时,编译器就会调用我们自己的删除器。
shared_ptr一般只需要在参数中添加具体的删除器函数名即可。

void myDelete(int *p)//删除器,用来删除整形指针用,当智能指针引用为0时,就会自动调用该删除器删除对象。
{
	delete p;//必须要的
}
void main()
{
	shared_ptr<int>p(new int (123),myDelete);//添加删除器
	shared_ptr<int>p2(p);//两个引用计数
	p2.reset();//剩一个引用计数
	p.reset();//释放该对象,调用删除器,同时p置空
	
}

删除器还可以是一个lambda表达式:

shared_ptr<int>p(new int(123),[](int *p){delete p;})

有些情况下,默认的删除器处理不了:动态数组
当数组中是类类型的时候,必须使用delete[]进行释放,此时系统默认的delete会报错,所以需要自己的删除器

shared_ptr<int>p(new int [10],[](int *p){delete p;})

shared_ptr<A>pA(new A[10]);//

shared_ptr<A>pA(new A[].[](A *p){delete[]p;})

用default_delete做删除器,可以删除数组

shared_ptr<A>pA(new A[10],std::fault_delete<A[]>);//使用A[]表示这是一个数组

在定义数字的时候在<>中加[]

shared_ptr<A[]>pA(new A[10]);//在定义时加A[],就可以使用系统默认的delete了
shared_ptr<int[]>pA(new int[10]);
p[0]= 12;//这种定义方式还支持下标

就算是两个shared_ptr指定了不同的删除器,只要指向的对象类型相同,那么这两个shared_ptr也属于同一个类型,可以放到一个容器中。

vector<shared_ptr<int>pvec(p1.p2)>;

注意使用make_shared后无法指定删除器。

weak_ptr

辅助shared_ptr(强指针)工作,是弱指针,也是个类模板。
weak_ptr绑定到shared_ptr上不会改变引用计数,其析构和构造函数不会影响引用计数。
当shared_ptr需要释放所指定的对象照常释放,不管是否有weak_ptr指向该对象。
能力弱,控制不了所指向对象的生存期。

弱引用的作用:监视shared_ptr(强引用)的声明周期,是一种对shared_ptr能力的扩充,不是独立的智能指针,不能操作所指向的资源。

auto pi = make_shared<int>(100);
weak_ptr<int>piw(pi);//piw弱共享pi,pi强引用计数不改变,但是弱引用计数会改变;强引用计数才决定对象生存期,弱引用计数不会。
weak_ptr<int>piw2;
piw2 = piw;//现在pi是强引用,piw和piw2是弱引用

lock():检查weak_ptr所指向的对象是否存在,如果存在,lock()就返回一个指向该对象的shared_ptr,如果不存在,就返回一个空的shared_ptr。

auto pi2 = piw.lock();//pi2是一个shared_ptr,引用计数加1,
if (pi2 != nullptr)//说明对象存在
{
	cout << "对象存在" << endl;
	*pi2 = 12;//改变对象值
}
else
{
	cout << "对象不存在" << endl;
}

weak_ptr能够判断指向的对象是否存在

常用操作
use_count():获取与该弱指针共享对象的其他shared_ptr的数量,即获得强引用计数

auto pi = make_sjared<int>(100);
auto pi2(pi);
weak_ptr<int>piw(pi);
int isc = piw.use_count();//2个
cout << isc << endl;

expired():是否过期的意思,表示指向的对象不存在,返回True

auto pi = make_sjared<int>(100);
auto pi2(pi);
weak_ptr<int>piw(pi);
int isc = piw.use_count();//2个
cout << isc << endl;
pi.reset();//释放,1个
pi2.reset();//释放,0个
if(piw.expired())
{
	cout << "对象已经过期" << endl;
}

reset()将该弱引用指针设置为空,不影响指向该对象的强引用数量,但指向该对象的弱引用数量减少

lock()

auto p1 = male_shared<int>(42);
weak_ptr<int>pw;
pw = p1;
if (pw.expired())
{
	auto p2 = pw.lock();//返回一个shared_ptr,并且强引用计数加1
	if (p2 != nullptr) 
	{
		//
	}
}
	
//当离开p2的范围,引用计数重新变为1

尺寸问题
weak_ptr和shared_ptr的尺寸一样大,是裸指针的两倍,即8个字节;其中4个字节的裸指针指向对象,4个字节指向很大的数据结构(控制块),控制块里有所指向对象的强引用计数和弱引用计数和其他数据。

shared_ptr使用场景、陷阱、性能说明、尺寸

如果不同shared_ptr变量来接受myfunc返回的结果,那么myfunc所产生的shared_ptr就会被销毁。

慎用裸指针:裸指针到智能指针之间不能进行隐式转换,必须显式定义;把一个普通裸指针绑定到shared_ptr上之后,智能指针负责内存管理,不能再用裸指针访问shared_ptr管理的内存了。一定不要用裸指针初始化多个shared_ptr,初始化一个智能指针之后不能再使用裸指针,否则会导致内存被释放两次产生异常。

慎用get()返回的指针:返回智能指针指向的对象对应的裸指针。get返回的指针不能delete,否则会产生异常。不能将其他智能指针绑到get()返回的指针上,和上面是一个道理。即get()得到的指针不能用来初始化另个一智能指针或者对另一个智能指针进行赋值。

不要把this指针作为作为shared_ptr返回,改用enable_shared_from_this。在外面创建CT对象的智能指针以及通过CT对象返回的this指针智能指针都是安全的。

避免循环引用,防止内存泄漏。

shared_ptr的尺寸是裸指针的2倍,weak_ptr尺寸是裸指针的2倍;
第一个裸指针指向的是这个智能指针所指向的对象,第二个裸指针指向一个很大的数据结构(控制块),这个控制块里边有引用计数、弱引用计数、其他数据(自定义删除器、内存分配器等)。这个控制块是由第一个指向对象的shared_ptr创建的。
控制块创建时机:make_shared分配并初始化一个对象,返回指向此对象的shared_ptr,所以,它总是能够创建一个控制块;

移动语义:复制要增加引用计数,移动不需要;移动构造函数快过构造函数,移动复制运算符快过拷贝赋值运算符。

unique_ptr

独占式的概念(专属所有权):同一时刻只能有一个unique_ptr指针指向这个对象(这块内存);当这个unique_ptr被销毁的时候,它所指向的对象也被销毁。
格式:
unique_ptr<指向的对象类型>智能指针变量名

unique_ptr<int>pi;//pi指向一个int的空智能指针
if (pi == nullptr)
{
	cout << "pi还是空指针" << endl;
}
unique_ptr<int>pi2(new int(105));//pi2指向105的int对象
make_unique函数

不支持指定的删除器语法,如果不用删除器,建议使用。

unique_ptr<int>p1 = make_unique<int>(100);
auto p2 = make_unique<int>(200);
unique_ptr<int>pi2(new int (105));

不支持的操作(独占式)

unique_ptr<string>ps1(new string("abc"));
unique_ptr<string>ps2(ps1);//不支持此操作,该智能指针不支持拷贝动作
unique_ptr<string>ps3 = ps1;//不支持
unique_ptr<string>ps4;
ps4 = ps1;//不支持

移动语义

unique_ptr<string>ps1(new string "abc");
unique_ptr<string>ps2 = std::move(ps1);//移动后,ps1为空,ps2指向原来ps1所指

release():放弃对指针的控制权(切断智能指针和其他所指向的对象之间的联系)
返回裸指针,将该指针置空。

unique<string>ps1 (new string "abc");
unique<string>ps2(ps1.release);//返回的是裸指针,ps2指向该裸指针指向的对象
if (ps1 == nullptr)//已经被置空
{
	cout << "ps1被置空" << endl;
}
string *temp = ps2.release();
delete temp;//要人工delete,不然内存泄漏

reset()
不带参数情况:释放智能指针指向的对象,并将智能指针置空

unique_ptr<string>ps1(new string("abc"));
ps1.reset();
if (ps1 == nullptr)
{
	cout << "被置空" << endl;
}

带参数的情况,释放智能指针所指向的对象,并让该智能指针指向新对象

unique_ptr<string>ps2(new string(""));
ps1.reset(ps2.release);//释放ps1的对象,并让ps1指向ps2所指向的对象(ps2.release返回裸指针),同时ps2置空(独占性) 
ps1.reset(new string ("abcd"));//清空ps1对象“abc”,使ps1指向“abcd”

=nullptr释放智能指针所指向的对象,并将智能指针置空

unique_ptr<string>ps1(new string "abc");
ps1 = nullptr;//清楚字符串“abc”,并清空ps1,不存在内存泄漏

指向一个数组

unique_ptr<int[]>array(new int [10]);//定义数组,注意[]
array[0] = 12;
array[1] = 12;
unique_ptr<A[]>array(new A[2]);//注意[],否则会报错

get()返回智能指针中的裸指针,要小心使用:当对象被释放,裸指针也就无效了
考虑到有些函数参数用的是裸指针,所以引入该函数。

unique_ptr<string>psl(new string ("abc"));
striing *ps = psl.get();//返回裸指针
*ps = "abcd";//给对象赋值

*解引用:获取该智能指针指向的对象,可以直接操作,但对于数组来说不成立

unique_ptr<string>psl(new string("abc"));
*psl = "abcd";//直接修改其对象

swap()用于交换两个智能指针所指向的对象

std::swap(ps1,ps2);
ps1.swap(ps2);//都可以

智能指针名字作为判断条件

unique_ptr<string>ps1(new string("abc"));
if (ps1)//与ps1 != nullptr是等效的
{
	cout << "不为空" << endl;
}

转换成shared_ptr类型:如果unique_ptr为右值,就可以将它赋值给shared_ptr
因为shared_ptr包含一个显式构造函数,可以将unique_ptr转换成shared_ptr。

shared_ptr<string>ps2 = myfunc();//函数return一个unique_ptr指针,这里会创建控制块
unique_ptr<string>ps(new string ("abc"));
shared_ptr<string>ps3 = std::move(ps);//左值转右值

返回unique_ptr、删除器、尺寸

返回unique_ptr
虽然unique_ptr智能指针不能拷贝,但是当其将要被销毁时是可以拷贝的。最常见的用法是从函数返回一个unique_ptr智能指针。

unique_ptr<string>ps;
ps = tuniqp();//可以用ps来接,则临时对象直接构造在ps里,如果不接,则临时对象会被释放,同时会释放指向的对象

指定删除器,delete是默认的删除器
格式:unique_ptr<所指向的对象类型,删除器>智能指针变量名
注意:删除器的位置和shared_ptr的位置不同,先要在类型模板参数中传递进去类型名,然后在参数中再给具体的删除其函数名。
shared_ptrp(new int(), mydelete)

void mydelete(string *pdel)
{
	delete pdel;
	pdel = nullptr;
}

//调用
typdef void (*fp)(string *);//定义一个函数指针类型,类型名为fp
unique_ptr<string,fp>psl(new string ("abc",mydelete));
//还可以
using fp2 = void(*)(string*);//使用using进行定义,效果与上面相同
//还可以
typdef decltype(mydeleter)*fp3;//这里多了一个*,因为decltype返回的是函数类型void (string *),加*表示函数指针类型,现在fp3应该void *(string*)
unique_ptr<string ,fp3>psl(new string ("abc"), mydeleter);
//还可以
unique_ptr<string ,decltype(mydeleter)*>ps4(new string ("abc"), mydeleter);
//还可以lambda,把lanbda理解成一个class
auto mydella = [](string *pdel){
	delete pdel;
	pdel  = nullptr;
}
unique_ptr<string ,decltype(mydella)*>ps5(new string ("abc"), mydeleter);

就算两个shared_ptr指定的删除器不同,只要所指对象相同,那么也同属于一个类型;
但是unique_ptr不一样,指定unique_ptr中的删除器会影响unique_ptr的类型;
删除器不同,shared_ptr可以放到一个容器中,但是unique_ptr不行。

通常,unique_ptr尺寸与裸指针的尺寸一样。如果使用函数指针作为删除器之后,尺寸增加一倍,所以自定义删除器会影响unique_ptr的效率。而shared_ptr始终固定。

总结(重要)

智能指针的主要目标:释放内存,防止忘记释放内存时产生内存泄漏;
智能指针的选择:
使用单个的用unique_ptr(尺寸小首选),
使用多个的用shared_ptr

并发与多线程

概念

并发:
两个或者更多的任务同时发生(进行);一个程序同时执行多个独立的任务;
之前的单核cpu由操作系统调度,每秒钟进行所谓的“任务切换”,是并发的假象;这种切换是有时间开销的,比如保存各种状态和进度;
多核cpu就可以并行执行多个任务(硬件并发);
并发的目的就是提高性能;

可执行程序:
磁盘上的文件,windows一个扩展名为.exe的文件,linux下./文件名,可执行的

进程:
可执行文件运行起来就是一个进程;

线程:
每个进程,都有一个主线程,并且主线程是唯一的;
当执行一个可执行程序,产生一个进程后,主线程就随之产生;当运行程序时,实际上是主线程来调用mian里面的代码,当进程结束,主线程也就结束;
线程可以理解为代码的执行通路,除了主线程之外,可以通过写代码创建其他的通路。每创建一个新线程,就可以在同一时刻多走一条不同的代码执行路径。

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

并发

实现并发手段:
a)通过多个进程实现并发;
进程之间需要通信。
b)通过多个线程来实现并发,自己写代码来创建除了主线程之外的其他线程;
单个进程中创建了多个线程,每个线程有自己的运行路径,但是一个进程中的所有线程共享地址空间(共享内存)。全局变量,指针,引用都可以在线程之间传递,使用多线程开销远远小于多进程。

多进程和多进程并发可以混合使用,优先考虑多线程计数手段而不是多进程、

和进程比,线程:
线程启动速度快,更轻量级;
系统西苑开销更少,执行速度快,比如共享内存;
使用有一定难度,要小心一致性问题。

新标准线程库

C++可编写可移植性(跨平台)的并发程序

线程启动、结束、创建线程方法、join、detach

程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;

int mian()
{
	cout <<"abc"<< endl;//实际上是主线程正在运行,主线程从mian函数返回,则整个进程执行完毕
	return ;
}

创建自己的线程,也需要从一个函数开始,一旦函数结束,则此线程结束;
整个进程执行完毕的标准是,主线程是否被执行完毕;
当主线程运行完毕,其他子线程也会被强行终止;所以要保持子线程,也要保证主线程的运行;

a)包含一个thread头文件
b)初始函数,当这个函数执行完毕,子线程就结束了
c)mian函数

#include 
void myprint()
{
	cout <<"线程开始执行"<<end;
	cout <<"线程执行完毕"<<end;
}
int mian()
{
	//创建线程,以myprint为入口开始执行
	thread mytobj(myprint);//myprint可调用对象,作为实参来构造thread对象
	//阻塞主线程并等待子线程执行完毕,如果没有这句话会出现主线程先执行完但是子线程没有执行完的情况导致出错
	mytobj.join();//阻塞主线程,让主线程等待子线程执行完毕,子线程执行完毕主线程继续执行
	
	cout <<"主线程" << endl;
	return ;
}

此代码有两个线程,相当于有两条平行线,即使一条线被堵住其他还是能够继续执行。

传统多线程程序主线程要等待子线程执行完毕,然后自己再最后退出;
detach分离:主线程和子线程无关,不需要再互相等待了;(不推荐)
一旦detach之后,与这个主线程关联的thread对象就会失去与主线程的关联,子线程就会驻留在后台运行,相当于被系统接管,子线程执行完毕后由运行时库负责清理。

int mian()
{
	//创建线程,以myprint为入口开始执行
	thread mytobj(myprint);//myprint可调用对象,作为实参来构造thread对象
	//阻塞主线程并等待子线程执行完毕,如果没有这句话会出现主线程先执行完但是子线程没有执行完的情况导致出错
	mytobj.detach();
	
	cout <<"主线程" << endl;
	return ;
}

joinable():
判断是否可以成功使用join()或者detach()的;
返回true(可以)或者false(不可以);

thread mytobj(myprint);
if (myjobj.joinable())
{
	cout <<"可以" << endl;
}
else
{
	cout << "不可以" << endl;
}

其他创建线程的手法

1)用类

class TA
{
public:
	void operator()()//不能带参数
	{
		//
	}
}
int main ()
{
	TA ta;
	thread mytaoj3(ta);//ta可调用对象
	//mytobj3.join();
	mytobj.detach();
	return 0;
}

主线程不再了,ta对象也不再了,但是由于detach(),子线程利用拷贝过来的ta对象还在进行。

2)用lambda表达式

auto mylambda =[]{
	//
};
thread mytobj4(mylambdathread);
mytobj4.join();

线程传参详解、detach()大坑、成员函数指针做线程函数

传递临时对象作为线程参数

void myprint(const int &i, char *pmybuf)
{
	cout <<i<<endl;
	cout << pmybuf << endl;
	return;
}
int mian()
{
	int mvar = 1;
	int &mvary = mvar;
	char mybuf[] = ""abc;
	thread mytobj(myprint,mvar,mybuf);
	mytobj.join();
	cout << "abcd"<< endl;
	return 0;
}

要避免的陷阱:
因为mvar是主线程中的对象,虽然使用了引用但其实是值传递(地址不同),子线程中仍然是安全的,这里detach不会出问题;但是当主线程结束,指针不能再使用(地址相同),所以这里不能使用detach,这里可以改成const&。
修改后的版本:

void myprint(const int &i, const char& pmybuf)//修改
{
	cout <<i<<endl;
	cout << pmybuf << endl;
	return;
}
int mian()
{
	int mvar = 1;
	int &mvary = mvar;
	char mybuf[] = ""abc;
	thread mytobj(myprint,mvar,mybuf);
	mytobj.join();
	cout << "abcd"<< endl;
	return 0;
}

实际上,存在主线程变量mybug已经被回收,已经无法再传入子线程的情况
修改后的版本

void myprint(const int &i, const char &pmybuf)
{
	cout <<i<<endl;
	cout << pmybuf << endl;
	return;
}
int mian()
{
	int mvar = 1;
	int &mvary = mvar;
	char mybuf[] = ""abc;
	thread mytobj(myprint,mvar,string(mybuf));//在这里改成string对象,就可以保证应用于子线程
	mytobj.join();
	cout << "abcd"<< endl;
	return 0;
}

在创建线程的同时构造临时对象的方法传递参数是可行的,一定能在主线程结束之前把子线程的参数构造出来,主线程结束了还能继续使用;

总结:
a)传递int这种简单的类型,建议值传递;
b)传递类类型,避免隐式转换,全部在创建子线程这一行就显式构造出临时对象,然后在函数参数里用引用来接,否则系统还会再构造一次;
c)建议不适用detach,这样就不存在局部变量失效导致对内存非法引用的问题。

线程id概念:

每个线程实际上都对应一个数字,且数字各不相同,使用std::this_thread::get_id()来获取。

传递类对象作为线程参数

std::ref函数:不用拷贝,而是传递真正的引用(之前有提到即使用引用地址还是发生了改变,不是真正的引用)

std::thread mytobj(myprint ,std::ref(myobj));//不会调用拷贝构造函数

注意:用了ref不能用detach

用成员函数指针做线程函数

使用成员函数

void thread_work(int num)
{
	cout <<"子线程执行"<<this<<"threadid = " << std::this_thread::get_id()<< endl;
}
A mtobj(10);//创建个对象
std::thread mytobj(&A::thread_work, myobj,15);//注意格式
mytobj.join();

使用可调用对象

void operator()(int num)
{
cout << "子线程执行"<<this<< "threadid="<<std::this_thread::get_id()<< endl;
}
A mtobj(10);//创建个对象
std::thread mytobj(std::ref(myobj),15);//
mytobj.join();

创建多个线程,数据共享问题分析

创建等待多个线程

void myprint(int inum)
{
	return ;
}
int mian()
{
	vector<thread> mythreads;
	for (int i = 0; i< 10; i++)
	{
		mythreads.push_back(thread(myprint,i));//放入容器中进行管理
	}
	for (auto iter = mythreads.begin(); iter!= mythreads.end(); ++iter )//迭代器
	{
		iter->join();//稳定
	}
	return 0;
}

执行顺序是乱的;所有子线程结束主线程才结束;
把thread对象放到容器里管理,看起来像个thread数组,方便一次创建大量线程和管理

数组共享问题分析

1)只读的数据
只读数据是安全稳定的

vector<int> g_v = {1,2,3};

2)有读有写
不崩溃处理:读的时候不能写,写的时候不能读;写只能一个写,读只能一个读;
3)其他

数据共享保护案例

两个线程,一个收集玩家命令并写到一个队列中,另外一个从队列中取出玩家发送的命令解析,然后执行玩家需要的动作。
list在频繁插入和删除数据时效率高,vector容器随机插入和删除的效率高

#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
		}
	}
	//取出的线程
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				msgRecQueue.pop_front();//移除第一个元素但不返回
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

此程序会报错,因为两个子线程在同时读写,一定会出错
代码化解决:
互斥量

互斥量概念、用法、死锁演示及解决

互斥量(mutex)

互斥是个类对象,理解成一把锁,多个线程尝试使用lock()陈冠函数来加锁这把锁头,只有一个线程能够锁定成功。如果没锁成功,流程就一直在加锁的过程上。
互斥量保护的数据不能多也不能少。
引入#include
a)lock(),unlock()
先加锁,操纵数据,再解锁;
lock()和ynlock()要成对使用;

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			my_mutex.lock();
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_mutex.unlock();
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				my_mutex.lock();
				msgRecQueue.pop_front();//移除第一个元素但不返回
				m_mutex.unlock();
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex;//创建互斥量的成员变量
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

为了防止忘记unlock(),引入了一个std::lock_guard的类模板(类似智能指针);
b)std::lock_guard()使用了之后就不再使用lock()unlock()
但是不如loc()unlock()灵活,只能在return处解锁;
提前析构的方法,加一个{}

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			{
				std::lock_guard(std::mutex)sbguard(my_mutex);//lock_guard里执行了lock()
				msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			}
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				{
					std::lock_guard(std::mutex)sbguard(my_mutex);//lock_guard里执行了lock()
					msgRecQueue.pop_front();//移除第一个元素但不返回
				}
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

死锁

前提是至少两把锁,即两个互斥量;
1)死锁演示

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			my_mutex1.lock();
			my_mutex2.lock();
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_mutex2.unlock();
			my_mutex1.unlock();
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				my_mutex2.lock();
				my_mutex1.lock();
				msgRecQueue.pop_front();//移除第一个元素但不返回
				m_mutex1.unlock();
				m_mutex2.lock();
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::mutex my_mutex2;
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

两个线程,一个先锁1,一个先锁2,出现死锁互相等待的情况;

2)解决方案:调用顺序一致
3)std::lock()函数模板:处理多个互斥量
一次锁住两个或者两个以上的互斥量,不会存在因为锁的顺序问题导致锁的风险问题。
要么两个都没锁,要么两个都锁住。如果其中一个没锁住,就睡把已经锁住的解锁。

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			std::lock(my_mutex1,my_mutex2);
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_mutex1.unlock();
			my_mutex2.unlock();
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				std::lock(my_mutex1,my_mutex2);
				msgRecQueue.pop_front();//移除第一个元素但不返回
				m_mutex1.unlock();
				m_mutex2.unlock();
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::mutex my_mutex2;
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

4)std::adopt_lock放到std::lock_guard()中可以在多个互斥量时自动unlock()
一个结构体对象,表示互斥量已经lock(),不需要对对象再次进行lock()

std::lock_guard<std::mutex>sbguard1(my_mutex1,std::adopt_lock);

std::lock()可以一次锁定多个锁,但是不常用;

unique_lock

unqiue_lock取代lock_guard

一般推荐unique_lock;

std::unique_lock<std::mutex>sbguard(my_mutex)

unique_lock的第二个参数

1)adopt_lock表示互斥量已经lock了,unique_lock也可以使用adopt_lock,前提是先要用lock()锁住

my_mutex.lock();//要先加锁,后续才能用adopt_lock
std::unique_lock<std::mutex>sbguard(my_mutex,std::adopt_lock);
std::chrono::milliseconds_drua(200000);
std:::this_thread::sleep_for(drua);

2)std::try_to_lock
尝试使用lock()去锁定mutex,如果没有锁定成功,也会立即返回,前提是不能先lock()

std::unique_lock<std::mutex>abguard(my_mutex,std::try_to_mutex);

3)std::defer_lock
没有给mutex加锁。使用前提是不能自己先lock,否则会报异常。

std::unique_lock<std::mutex>sbguard(my_mutex,std::defer_lock);//没有加锁的my_mutex

unique_lock的成员函数

1)lock()
2)unlock()

std::unique_lock<std::mutex>sbguard(my_mutex,std::defer_lock);//没有加锁的my_mutex
sbguard.lock();//不用自己unlock()
//处理共享代码
sbguard.unlock();
//处理一些共享代码
sbguard.lock();
//处理共享代码
...

3)try_lock

std::unique_lock<std::mutex> sbguard(my_mutex,std::defer_lock);//没有加锁的my_mutex
if(sbguard.try_lock == true)//返回true表示拿到锁了
{
}
else
{
}

4)release()
返回所管理的mutex对象指针,并释放所有权;这个mutex_lock和mutex不再有关系;
注意和unlock()的区别;
如果原来mutex对象处于加锁状态,有责任接管过来并负责解锁(release返回的是原始muyex的指针)。

unique_lock所有权传递

单例设计模式共享数据分析、解决

设计模式

代码的一些写法;
把项目的开发经验、模块划分经验,总结成设计模式;

单例设计模式

整个项目中,由某个特殊类,属于该类的对象,只创建1个;
私有化构造函数,使得类外不能创建类对象;
只能通过调用成员函数生成唯一的类对象;

class MyCAS
{
privateMyCAS() {};//私有化构造函数,使得类外不能创建类对象

	static MyCAS *m_intance;//静态成员变量,指向对象的指针
public:
	static MyCAS *GetInstance()
	{
		if (m_intance == NULL)
		{
			m_instance = new MyCAS();//如果指针为空则创建一个类对象
		}
		return m_instance;
	}
	
		
	void func()
	{
		cout << "测试" <<endl;
	}
}
//类静态成员初始化

MyCAS *P_a = MyCAS::GetInstance();//创建一个对象,返回该类对象的指针
MyCAS *P_a = MyCAS::GetInstance();//第二次再调用时,返回的是上面相同的指针

只要调用这个成员函数,返回的始终是这个对象,为单例设计模式;
改进版:释放对象内存

class MyCAS
{
privateMyCAS() {};//私有化构造函数,使得类外不能创建类对象

	static MyCAS *m_intance;//静态成员变量,指向对象的指针
public:
	static MyCAS *GetInstance()
	{
		if (m_intance == NULL)
		{
			m_instance = new MyCAS();//如果指针为空则创建一个类对象
			static CGarhuishou cl;//静态对象,当程序退出时才进行析构,执行下面类的析构函数
		}
		return m_instance;
	}
	
	class CGarhuishou//类中类,用来释放大类的对象
	{
	public:
		~CGarhuishou()//析构
		{
			if(MyCAS::m_instance)
			{
				delete MyCAS::m_instance;//对应new
				MyCAS::m_instance = NULL;
			}
		}
	}
		
	void func()
	{
		cout << "测试" <<endl;
	}
}
//类静态成员初始化

MyCAS *P_a = MyCAS::GetInstance();//创建一个对象,返回该类对象的指针
MyCAS *P_a = MyCAS::GetInstance();//第二次再调用时,返回的是上面相同的指针

单例设计模式

建议主线程在其他线程开始之前就初始化各种对象,变为只读数据,子线程可以安全访问此数据

MyCAS *p_a = MyCAS::Getinstance();
p_a->func();

共享数据问题分析

面临的问题:需要在子线程中创建单例类的对象,可能不止一个。

void mythread()
{
	MyCAS *p_a = MyCAS::GetInstance();
	return;
}

std::thread mytobj1(mythread);
std::thread mytobj2(mythread);
mytobj1.join();
mytobj2.join();

这两个线程都去执行类里新建对象函数;
加一个互斥量防止同时调用:

std::mutext resource_mutex;
void mythread()
{
		if(m_instance==NULL)//双重检查提高效率
		{
			std::unique_lock<std::mutex> mymtex(resource_mutex);//自动加锁
			if(m_instance == NULL)
			{
				MyCAS *p_a = MyCAS::GetInstance();
				return;
			}
		}
		return m_instance;
}

std::thread mytobj1(mythread);
std::thread mytobj2(mythread);
mytobj1.join();
mytobj2.join();

std::call_once()

该函数第二个参数是一个函数名a()l
具备互斥量这种能力,而且效率更高;
call_once()需要与一个标记结合使用,这个标记std::once_flag,其实once_flag是一个结构;
call_once()就是根据这个标记来决定a()是否执行,调用call_once()成功后,就把这个标记设置为一种一调用的状态,只要是已调用状态,对应的函数a()就不会再被执行了。

std::mutex resource_mutex;
std::once_flag g_flag;//标记
class MyCAS
{
	static void CreateInstance();//只调用一次
	{
		m_instance = new MyCAS();//如果指针为空则创建一个类对象
		static CGarhuishou cl;//静态对象,当程序退出时才进行析构,执行下面类的析构函数
	}
privateMyCAS() {};//私有化构造函数,使得类外不能创建类对象

	static MyCAS *m_intance;//静态成员变量,指向对象的指针
public:
	static MyCAS *GetInstance()
	{
		std::call_once(g_flag,CreateInstance);//一个线程等另一个线程执行完毕函数,g_flag可以看成一把锁
		return m_instance;
	}
	
	class CGarhuishou//类中类,用来释放大类的对象
	{
	public:
		~CGarhuishou()//析构
		{
			if(MyCAS::m_instance)
			{
				delete MyCAS::m_instance;//对应new
				MyCAS::m_instance = NULL;
			}
		}
	}
		
	void func()
	{
		cout << "测试" <<endl;
	}
}
//类静态成员初始化

MyCAS *P_a = MyCAS::GetInstance();//创建一个对象,返回该类对象的指针
MyCAS *P_a = MyCAS::GetInstance();//第二次再调用时,返回的是上面相同的指针

条件变量condition_variable、wait()、notify_one()

双重锁定

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		//双重锁定,多加一层判断
		if (!msgRecQueue.empty())
		{
			std::unique_lock<std::mutex>sbguard1(my_mutex1);
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
				msgRecQueue.pop_front();//移除第一个元素但不返回
				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::mutex my_mutex2;
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

使用多层判断效率会降低;
使用条件变量
std::condition_variable是一个类,等待一个条件达成;
需要和互斥量配合使用;
wait有两个参数,如果第二个参数lambda返回值是false/没有第二个参数,那么wait将解锁互斥量,并堵塞到本行,堵到其他线程调用notify_one()成员函数为止;如果第二个参数返回true,那么wait直接就返回;
notify_one()尝试把wait的线程唤醒

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			std::unique_lock<std::mutex>sbguard1(my_mutex1);
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_cond.notify_one();//尝试把wait的线程唤醒
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		int command = 0;
		while(true)
		{
			std::unique_lock<std::mutex>sbguard1(my_mutex1);
			//wait有两个参数,如果第二个参数lambda返回值是false/没有第二个参数,那么wait将解锁互斥量,并堵塞到本行,堵到其他线程调用notify_one()成员函数为止;如果第二个参数返回true,那么wait直接就返回
			//当其他线程用notify_one唤醒wait,wait就开始不断尝试重新获取互斥量锁,如果获取不到流程就卡在这里等着获取;如果wait第二个参数为false,就解锁互斥量,开始休眠等待再次被notify_one唤醒;如果表达式为true,则wait返回,流程走下来(互斥锁被锁着);如果没有第二个参数,则wait返回
			my_cond.wait(sbguard1,[this]{
				if(!msgRecQueue.empty())
					return true;
				return false;
			});
			//流程到这里锁一定锁着的.且至少有一条数据
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			sbguard.unlock();
		}
		

	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::condition_variable my_cond;//生成一个条件变量对象
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

async、future、packaged_task、promise

std::async、std::future创建后台任务并返回值

希望线程返回一个结果;
std::async是一个函数模板,用来启动一个异步任务,返回一个std::future对象;
自动创建一个线程,并开始执行对应线程入口函数,返回一个std::future对象,里面含有线程入口函数所返回的结果,可以通过调用future对象的成员函数get()来获取结果;
std::future提供了一种访问异步操作结果的机制,在线结束时能拿到结果;

#include 
int mythread()
{
	cout <<"threadid="<< std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//等待5s
	std::this_thread::sleep_for(dura);
	return 5;
}

//main
cout << "threadid="<<std::this_thread::get_id()<< endl;//主线程
std::future<int>result = std::async(mythread);//创建一个线程
int def;
def = 0;
cout <<result.get()<< endl;//等待子线程返回拿到结果

通过std::future对象的get()成员函数等待线程执行结束并返回结果;
get()不拿到返回值就就会一直卡在这里不进行下去;
还有一个wait()和get()一样也会卡在这里,但是get()可以接受返回值,wait()不能;
get()只能调用一次;

引用类中成员函数的情况:

#include 

class A
{
public:
	int mythread(int mypar)
	{
		cout <<"threadid="<< std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);//等待5s
		std::this_thread::sleep_for(dura);
		return 5;
	}

//main
A a;
cout << "threadid="<<std::this_thread::get_id()<< endl;//主线程
std::future<int>result = std::async(&A::mythread,&a,tmppar);//第二个参数是对象引用,才能保证线程里用的是同一个函数
int def;
def = 0;
cout <<result.get()<< endl;//等待子线程返回拿到结果

通过额外向std::async()传递一个参数,该参数类型是std::lunnch类型,来达到一些特殊的目的:
a)std::launnch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行(实际上线程就没创建,直到调用get()和wait());
b) std::launnch::async:表示在调用async()函数的时就开始创建新子线程(async默认的就是这种情况);

std::package_task

打包任务:把任务包装起来
是个类模板,它的模板参数是各种可调用对象包装起来,方便将来作为线程入口函数;

#include 
int mythread(int mypar)//线程入口函数
{
	cout <<"threadid="<< std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//等待5s
	std::this_thread::sleep_for(dura);
	return 5;
}

//main
cout << "threadid="<<std::this_thread::get_id()<< endl;//主线程
std::package_task<int(int)> mypt(mythread);//把函数mythread通过package包装起来
std::thread t1(std::ref(mypt),1);//创建线程,1作为线程入口函数的参数
t1.join();//等待线程执行完毕
std::future<int>result = mypt.get_future();//future里保存的就是mypt里面的返回值,即线程入口函数的返回结果
cout<< result.get()<< endl;//上面线程已经返回了

包装lambda表达式

std::package_task<int(int)> mypt ([]mypar){
	//
}

package_task包装起来的对象可以直接调用,也是一个可调用对象;

mypt(105);//直接调用,相当于函数调用
std::future<int>result = mypt.get_future();
cout<< result.get()<<endl;

容器包装

vector<std::packaged_task<int(int)>> mytasks;//容器
mytasks.push_back(std::move(mypt));//用了移动语义,入了之后 mypt就为空

std::package_task<int(int)>mypt2;
auto iter mytasks.begin();
mypt2 = std::move(*iter);//移动语义
mytasks.erase(iter);//删除第一个元素,迭代已经失效了,后面不能再用iter了

std::promise

类模板,能在线程中给它赋值,然后在其他线程中取出来

void mythread(std::promise<int> &tmpp, int calc)
{
	//
	int result =calc;
	tmpp.set_value(result);//结果保存到了tempp对象中
}

//
std::promise<int> myprom;
std::thread t1(mythread, std::ref(myprom),180);
t1.join();
//获取结果
std::future<int> fu1 = myprom.get_future();//promise和future绑定用于获取线程返回值
auto result = ful.get();
cout << rseult <<endl;

promise可以保存一个值,在将来某个时刻通过把一个future绑定到promise上来获得这个保存的值;

future其他成员函数、shared_future、atomic

其他成员函数

#include 
int mythread()
{
	cout <<"threadid="<< std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//等待5s
	std::this_thread::sleep_for(dura);
	return 5;
}

//main
cout << "threadid="<<std::this_thread::get_id()<< endl;//主线程
std::future<int>result = std::async(mythread);//创建一个线程
//枚举类型
std::future_status status = result.wait_for(std::chrono::second(6));//
if (status == std::future_status::timeout)//超时表示线程没执行完
{
	//超时
}
else if(status ==std::future_status::ready)//线程已返回
{
	//执行完毕
	cout <<result.get()<<endl;
}
else if (status == std::future_status::deferred)
{
	//如果async的第一个参数设置为延迟执行std::launch::deferred,则本条件成立
	cout << result.get()<< endl;//未执行子线程
}
 

std::shared_future

之前get只能调用一次的原因是,get是移动语义;
为了使多个线程可以获得数据,使用shared_future,它的get不再是转移数据而是复制数据;

std::future<int>result = mypt.get_future();//通过get_future构造了一个shared_future对象
std::shared_future<int>result_s(result.shared());//把result里的值移动到result_s里
auto mythreadresult = result_s.get();

std::thread t2(mythread,std::ref(result_s));
t2.join(); 

原子操作std::automic

1)原子操作概念
互斥量:多线程中 保护共享数据-锁;
有两个线程,只对一个变量进行操作,一个线程读,一个线程写;

int ayomvalue;
//读线程
int tmpvalue = atomvalue;//代表多线程之间共享的变量
//写线程
atomvlaue = 6;
int g_mycout = 0;//
void mythread()
{
	for (int i = 0;i<1000000;,i++)
	{
		g_cout ++;
	}
}
//
thread mytobj1(mythread);
thread mytobj2(mythread);
mytobj1.join();
mytobj2.join();

两个线程同时对一个变量处理会出现问题,最终结果不是100000*2;
一种解决办法:互斥量(加锁)

int g_mycout = 0;
std::mutex g_my_mutex;
void mythread()
{
	for (int i = 0;i<1000000;,i++)
	{
		g_my_mutex.lock();
		g_cout ++;
		g_my_mutex.unlock();
	}
}
//
thread mytobj1(mythread);
thread mytobj2(mythread);
mytobj1.join();
mytobj2.join();

加锁后结果正确,但是由于++过程被打断,效率会降低;
另一种方法:原子操作,不需要用到互斥量加锁的多并发编程方式;
原子操作在多线程中不会被打断,效率更高;
但只适用于一个变量而不是一段代码;
原子的意思:要么是完成的,要么是没完成的,不会是半完成的中间状态,是不可分割的;
std::atomic是一个类模板,用来封装某个类型的值;
2)用法范例

std::atomic<int> g_mycout = 0;//封装了一个类型为int的对象(值),具有原子操作的类对象

void mythread()
{
	for (int i = 0;i<1000000;,i++)
	{
		g_cout ++;
	}
}
//
thread mytobj1(mythread);
thread mytobj2(mythread);
mytobj1.join();
mytobj2.join();

结果正确,而且效率更高

布尔型范例

std::atomic<bool> g_ifend = false;//线程退出标记,原子操作
void mythread()
{
	std::chrono::millseconds dura(1000);
	while(g_ifend == false)
	{
		cout << std::this_thread::get_id()<< endl;
		std::thread::sleep_for(dura);
	}
}
//
thread mytobj1(mythread);
thread mytobj2(mythread);
std::chrono::millseconds dura(5000);
std::thread::sleep_for(dura);
g_ifend = true;//对原子对象的写操作,让线程自行运行结束
mytobj1.join();
mytobj2.join();

3)总结
原子操作只针对变量,作用域有限,通常用于计数或者统计;

std::atomic、std::async

std::atomic

上例中的g_cout++改成g_cout = g_cout+1就错误了;
一般是针对++,–,+=,&=,!=,^=是支持的没其他可能不支持;

std::async参数

int mythread()
{
	cout <<std::this_thread::get_id()<< endl;//线程id
	return 1;
}
//
cout <<std::this_thread::get_id()<< endl;//主线程id
std::future<int> result = std::async(mythread);//启动子线程
cout <<result.get()<<endl;//获得子线程结果

async两个参数:std::launch::deferred延迟调用和std::launch::async强制创建一个线程;
如果系统资源紧张,可能出现线程创建失败程序崩溃;
async一般叫创建一个异步任务,与thread最明显的不同是async有时并不创建新线程;
std::launch::deferrred延迟调用,延迟到future对象调用get或者wait时才执行mythread;
std::launch::async强制异步任务在新线程上执行;
std::launch::async|std::launch::deferrred同时用,则可能是创建新线程也可能是延迟调用;
当不带额外参数时,只给一个入口函数名mythread,默认和上面一样,二者选一;

async和thread的区别:
1)thread创建线程如果失败程序会崩溃;而async不会报异常,无法创建时就不会创建线程,等待get或者wait再创建;
2)async可能创建也可能不创建线程,而且很容易拿到线程的返回值;

async不确定性问题:
当两个参数都加或者不加参数的时候,系统如何决定是否延迟调用?
std::future的wait_for()函数:

#include 
int mythread()
{
	cout <<"threadid="<< std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//等待5s
	std::this_thread::sleep_for(dura);
	return 5;
}

//main
cout << "threadid="<<std::this_thread::get_id()<< endl;//主线程
std::future<int>result = std::async(mythread);//创建一个线程
//枚举类型
std::future_status status = result.wait_for(0s);//
if (status == std::future_status::deferred)
{
	//如果async的第一个参数设置为延迟执行std::launch::deferred,则本条件成立
	cout << result.get()<< endl;//未执行子线程
}
else
{
	if (status == std::future_status::timeout)//超时表示线程没执行完
	{
		//超时(资源紧张,采用deferred了)
		cout << result.get()<<endl;
	}
	else if(status ==std::future_status::ready)//线程已返回
	{
		//执行完毕
		cout <<result.get()<<endl;
	}
}

widows临界区、其他互斥量

windows临界区

进入临界区等于加锁;
离开临界区等于解锁;


#include 
#include 
#define_WINDOWSJQ1_
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
#define_WINDOWSJQ_
			EnterCriticalSection(&my_winsec);
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			LeaveCriticalSection(&my_winsec);
#else
			my_mutex.lock();
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_mutex.unlock();
#endif
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
#define_WINDOWSJQ_
			EnterCriticalSection(&my_winsec);
			msgRecQueue.pop_front(i);//将收到的数字放到消息队列中
			LeaveCriticalSection(&my_winsec);
#else
				my_mutex.lock();
				msgRecQueue.pop_front();//移除第一个元素但不返回
				m_mutex.unlock();
#endif				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
#ifdef _WINDOWSJQ_
	InitializeCriticalSection(&my_winsec);//用临界区之前先要初始化
#endif
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex;//创建互斥量的成员变量
#ifdef _WINDOWSJQ_
	CRITCAL_SECTION my_vinsec;
#endif
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

临界区和mutex实现技术很类似

多次进入临界区实验

在同一个线程,相同的临界区变量可以多次进入临界区,但到有相应次数的离开;
但是这种情况不能连续使用lock()同一个互斥量多次;
不同线程,会等待;

自动析构技术

std::lock_guard自动加锁自动解锁;
也不能对同一个互斥量多次加锁;
windows中类似lock_guard功能的代码,自动解锁以防死锁:

#include 
#include 
#define_WINDOWSJQ1_
class CWinLock//专门建立一个类,RAII类,专门用于资源获取和初始化类;容器智能指针都算RAII类
{
public:
	CWinLock(CRITICAL_SECTION *m_pCritmp)
	{
		m_pCritical=pCritmp;
		EnterCriticalSection(m_pCritical);
	}
	~CWinLock()
	{
		LeaveCriticalSection(m_pCritical);
	}
private:
	CRITICAL_SECTION *m_pCritical;
}
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
#define_WINDOWSJQ_
			//EnterCriticalSection(&my_winsec);
			CWinLock wlock(&my_winsec);//调用类,自动构造和析构
			CWinLock wlock(&my_winsec1);//可进入多次
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中

#else
			my_mutex.lock();
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_mutex.unlock();
#endif
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			if (!msgRecQueue.empty())
			{
				int command = msgRecQueue.front();//返回第一个元素但不检查是够存在
#define_WINDOWSJQ_
			EnterCriticalSection(&my_winsec);
			msgRecQueue.pop_front(i);//将收到的数字放到消息队列中
			LeaveCriticalSection(&my_winsec);
#else
				my_mutex.lock();
				msgRecQueue.pop_front();//移除第一个元素但不返回
				m_mutex.unlock();
#endif				//处理数据
			}
			else
			{
				cout << "没有数据" << endl;
			}
		}
	}
#ifdef _WINDOWSJQ_
	InitializeCriticalSection(&my_winsec);//用临界区之前先要初始化
#endif
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex;//创建互斥量的成员变量
#ifdef _WINDOWSJQ_
	CRITCAL_SECTION my_vinsec;
#endif
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

递归的独占互斥量recursive_mutex

std::mutex独占互斥量,自己拿到锁时其他人无法拿到锁;
递归独占:解决多次lock的问题,允许同一个互斥量被多次lock;

std::lock_guard<std::recursive_mutex> sbguard(my_mutex); 

带超时的std::timed_mutex和std::recursive_timed_mutex

std::timed_mutex:
try_lock_for():等待一段时间,如果拿到了锁或者到了时间没拿到锁,就往下走;
返回的true,则表示拿到锁,继续下去;
返回ture,则表示没拿到锁,也不等了继续下去;

std::chrono::millseconds timeout(100);
if(my_mutex.try_lock_for(timeout))
{
	msgRecQueue.push_back(i);
	my_mutex.unlock();
}
else
{
	std::chrono::microseconds sleeptime(100000);
	std::this_thread::sleep_for(sleeptime);
}

try_lock_unit()
参数是一个时间点,在时间点没到时拿到锁则继续下去,如果到了时间点没拿到也继续下去;

if(my_mutex.try_lock_unit1(chrono::steady_clock::now() + timeout))

std::recursive_timed_mutex带超市功能的递归独占互斥量

补充知识、线程池、数量

虚假唤醒

会出现没有数据也把wait唤醒的情况,就是虚假唤醒,会造成不安全;
加一个为空判断可以有效对数据进行判断;

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for (int i = 0; i< 1000 ;++i)
		{
			cout << i << endl;
			std::unique_lock<std::mutex>sbguard1(my_mutex1);
			msgRecQueue.push_back(i);//将收到的数字放到消息队列中
			my_cond.notify_one();//尝试把wait的线程唤醒
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		int command = 0;
		while(true)
		{
			std::unique_lock<std::mutex>sbguard1(my_mutex1);
			//wait有两个参数,如果第二个参数lambda返回值是false/没有第二个参数,那么wait将解锁互斥量,并堵塞到本行,堵到其他线程调用notify_one()成员函数为止;如果第二个参数返回true,那么wait直接就返回
			//当其他线程用notify_one唤醒wait,wait就开始不断尝试重新获取互斥量锁,如果获取不到流程就卡在这里等着获取;如果wait第二个参数为false,就解锁互斥量,开始休眠等待再次被notify_one唤醒;如果表达式为true,则wait返回,流程走下来(互斥锁被锁着);如果没有第二个参数,则wait返回
			my_cond.wait(sbguard1,[this]{
				if(!msgRecQueue.empty())//处理虚假唤醒,如果数据为空就不唤醒
					return true;
				return false;
			});
			//流程到这里锁一定锁着的.且至少有一条数据
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			sbguard.unlock();
		}
		

	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::condition_variable my_cond;//生成一个条件变量对象
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutMsgObj.join();

atomic

原子对象不允许复制,因为拷贝构造函数被禁止,同时定义时也不能进行赋值

#include 
#include 
//使用成员函数作为线程函数
class A{
public:
	atomic<int> atm;//原子
	A()
	{
		atm = 0;
	}
	//把收到的消息入到一个队列
	void inMesgRecQueue()
	{
		for(int i = 0; i<10000;i++)
		{
			atm++;//写数据
		}
	}
	//取出的线程
	void outMesgRecQueue()
	{
		while(true)
		{
			cout<<atm<<endl;//读数据,atm是个原子操作,但这一行不是原子操作
		}
	}
private:
	std::list<int>msgRecQueue;//用于装数据,在这里是共享数据
	std::mutex my_mutex1;//创建互斥量的成员变量
	std::condition_variable my_cond;//生成一个条件变量对象
};
//mian
A myobja;
std::thread muOutMsgQbj(&A::outMsgRecvQueue,&myobja);//引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myInMsgObj2.join();
myOutMsgObj.join();

线程池

场景设想:
每一个客户端创建一个新线程:
但当客户较多时不可取;
随时创建线程,稳定性差;

线程池:把一堆线程统一管理,循环利用线程的方式就是线程池;

实现方式:一次性创建一定数量的线程,这样是程序代码更稳定。

线程数量问题

线程数量极限:一般2000;
线程创建数量建议:
一般与cpu的数量有关;
一个线程等于一条执行通路,比堵塞的情况要稍多一些线程;
线程不是越大越好,切换和调度也会耗费资源;

多线程总结

你可能感兴趣的:(C++)