目录
并发与多线程
一、并发、进程、线程的基本概念
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_one
和notify_all
的效果是一样的。
八、创建后台任务
1、返回值的线程
std::async
和std::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)心得
一般用于计数,比如统计发送数据包