最近开始学习C++多线程内容,将笔记整理发布,可供参考和自己复习
开发环境
基于C++11标准,环境为vs2019
并发、进程、线程的基本概念和综述
并发
两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务;
以往计算机为单核cpu,进程切换是根据时间的上下文切换,产生并发的假象,这种上下文切换是由时间开销的。
硬件发展,出现多处理器多核计算机,能够真正并行实现多个任务(硬件并发)。
可执行程序
磁盘上的一个文件,例如:Windows下的一个扩展名为.exe的文件。Linux下的 ls, -la文件。
进程
已知可执行程序能够运行。Windows下双击一个可执行程序来运行或者Linux下 ./文件名之后,可执行从运行起来,就叫做创建了一个进程。(并不是很准确)
线程
每个进程都有一个主线程,并且这个主线程是唯一的,也就是一个进程中只能有一个主线程。
当你执行可执行程序,产生了一个进程后,这个主线程就随着这个进程启动了。运行程序时实际上时进程的主线程来调用代码的。
线程就是执行代码的!主线程和进程是同时存在也是同时消失的!
除了主线程外,可以通过自己写代码来创建其他的线程,其他线程走的是别的道路,执行不同的代码流程。每创建一个新线程,就可以在同一时刻多做一个不同的事情(多走一条不同的代码路径)。
多线程(并发)不是越多越好,每个线程都需要一个独立的堆栈空间。线程之间的切换要保存很多中间状态,上下文切换很频繁,耗费本该属于程序运行的时间。
线程启动、结束、创建多线程法、join、detach
线程的开始和结束
程序运行生成一个进程,进程运行代表该进程所属的主线程开始自动运行;
主线程从main函数开始执行。自己的线程也需要从一个线程开始执行(初始函数),一旦这个函数运行完毕,代表线程运行结束。
整个进程是否执行完毕的标志是主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了;此时,一般情况下,如果其它子线程还没有执行完毕,那么这些子线程也会被操作系统强行终止。
所以,一般情况下:想保持子线程的运行状态的话,那么就要让主线程一直保持运行,不要让主线程运行完毕。
#include<thread>
#include <iostream>
using namespace std;
//创建自己的函数
void myprint()
{
cout << "子线程开始执行" << endl;
//.........
//........
cout << "子线程执行完毕" << endl;
}
int main()
{
//(a) 包含头文件thread
//(b) 创建函数
//(c) main中开始写代码
thread mytobj(myprint);//myprint可调用对象
mytobj.join();
cout << "主线程执行完毕 " << endl;
return 0;
}
运行结果:
子线程开始执行
子线程执行完毕
主线程执行完毕
此程序中,主线程和子线程在同时运行,无论那一条线被阻塞,另一条线都可以运行。
thread类
thread是标准库里的一个类。thread mytobj(myprint);
创建了线程,线程执行起点myprint(); (2) myprint开始执行。
join()
join()阻塞主线程,让主线程等待子线程执行完毕,然后主线程和子线程汇合,然后主线程才往下运行。mytobj.join();
主线程阻塞到等待myprint()执行完毕,主线程就继续往下执行。
如果主线程执行完毕,子线程没有执行完毕,程序是不稳定不合格的。一个书写良好的程序,应该是主线程等待子线程执行完毕后,自己才能退出。
detach()
一般情况下,主线程要等待子线程执行完毕,自己再最后退出。
detach()主线程不和子线程汇合,主线程不必等子线程执行结束,并不影响子线程的执行。
为什么引入detach(): 如果创建很多子线程,让主线程等待子线程结束,不是很好,所以引入detach()。
一旦detach之后,与这个主线程关联的thread对象就会失去与主线程的关联。此时这个子线程就会驻留在后台运行,这个子线程就相当于被C++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程的相关资源(守护线程)。
#include<thread>
#include <iostream>
using namespace std;
//创建自己的函数
void myprint()
{
cout << "子线程开始执行" << endl;
//.........
//........
cout << "子线程执行完毕1" << endl;
cout << "子线程执行完毕2" << endl;
cout << "子线程执行完毕3" << endl;
cout << "子线程执行完毕4" << endl;
cout << "子线程执行完毕5" << endl;
cout << "子线程执行完毕6" << endl;
cout << "子线程执行完毕7" << endl;
cout << "子线程执行完毕8" << endl;
cout << "子线程执行完毕9" << endl;
cout << "子线程执行完毕10" << endl;
}
int main()
{
//(a) 包含头文件thread
//(b) 创建函数
//(c) main中开始写代码
thread mytobj(myprint);
mytobj.detach();
cout << "主线程执行完毕1 " << endl;
cout << "主线程执行完毕2 " << endl;
cout << "主线程执行完毕3 " << endl;
cout << "主线程执行完毕4 " << endl;
cout << "主线程执行完毕5 " << endl;
cout << "主线程执行完毕6 " << endl;
cout << "主线程执行完毕7 " << endl;
cout << "主线程执行完毕8 " << endl;
cout << "主线程执行完毕9 " << endl;
cout << "主线程执行完毕10 " << endl;
cout << "主线程执行完毕11 " << endl;
cout << "主线程执行完毕12 " << endl;
cout << "主线程执行完毕13" << endl;
cout << "主线程执行完毕14 " << endl;
cout << "主线程执行完毕15" << endl;
cout << "主线程执行完毕16 " << endl;
cout << "主线程执行完毕17 " << endl;
cout << "主线程执行完毕18 " << endl;
return 0;
}
执行结果:
主线程执行完毕1
主线程执行完毕2
主线程执行完毕3
主线程执行完毕4
主线程执行完毕5
主线程执行完毕6
主线程执行完毕7
主线程执行完毕8
主线程执行完毕9
主线程执行完毕10
主线程执行完毕11
主线程执行完毕12
主线程执行完毕13
主线程执行完毕14
主线程执行完毕15
主线程执行完毕16
主线程执行完毕17
主线程执行完毕18
子线程开始执行
vs2019似乎改为了主线程先执行完毕然后在运行时库中运行子线程,也有可能是cpu的问题?
**一旦调用了detach(),就不能再用join();**否则系统报告异常。
joinable()
判断能否成功调用 join() 或者 detach() 。返回 true 或者 false 。
int main()
{
//(a) 包含头文件thread
//(b) 创建函数
//(c) main中开始写代码
thread mytobj(myprint);
if (mytobj.joinable())
{
cout << "1:joinable == ture" << endl;
}
else
{
cout << "1:joinable == false" << endl;
}
mytobj.detach();
if (mytobj.joinable())
{
cout << "2:joinable == ture" << endl;
}
else
{
cout << "2:joinable == false" << endl;
}
结果为:
1:joinable == ture
2:joinable == false
其他创建线程的手法
类的方法
#include<thread>
#include <iostream>
using namespace std;
class TA
{
public:
void operator()()//不能带参数 重载()操作符使这个函数变为可调用对象
{
cout << "子线程opertator()开始执行" << endl;
//。。。。
cout << "子线程opertator()执行完毕" << endl;
}
};
int main()
{
TA ta;
thread mytobj3(ta);//ta是个可调用对象.
mytobj3.join();
cout << "主线程执行完毕1 " << endl;
return 0;
}
结果:
子线程operator()开始执行
子线程operator()执行完毕
主线程执行完毕1
容易出现的问题:
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class TA
{
public:
int& m_i;
TA(int& i) :m_i(i){}
public:
void operator()()//不能带参数 重载()操作符使这个函数变为可调用对象
{
cout << "m_i1的值为:" << m_i << endl;
cout << "m_i2的值为:" << m_i << endl;
cout << "m_i3的值为:" << m_i << endl;
cout << "m_i4的值为:" << m_i << endl;
cout << "m_i5的值为:" << m_i << endl;
cout << "m_i6的值为:" << m_i << endl;
}
};
int main()
{
int myi = 6;
TA ta(myi);
thread mytobj3(ta);//ta是个可调用对象.
mytobj3.detach();
cout << "主线程执行完毕1 " << endl;
Sleep(10000);
return 0;
}
int& m_i;
使用引用,引用为取地址符,此时主线程执行完成后,m_i被主线程释放,detach() 之后调用的m_i就会出错。所以最好不要用引用构建对象。
为何调用death() ,主线程执行结束后,对象 ta 还可以调用?
首先,对象 ta 已经被析构,但是对象 ta 实际上是被复制到线程中去了,所以执行完主线程后,ta 被销毁,但是复制的 ta 对象依旧存在,只要对象中没有引用,指针,就不会产生问题。
修改代码中类的程序来查看对象:
class TA
{
public:
int& m_i;
TA(int& i) :m_i(i)
{
cout << "TA构造函数执行" << endl;
}
TA(const TA& ta):m_i(ta.m_i)
{
cout << "TA的拷贝构造函数被执行" << endl;
}
~TA()
{
cout << "TA的析构函数被执行" << endl;
}
public:
void operator()()//不能带参数 重载()操作符使这个函数变为可调用对象
{
//cout << "子线程opertator()开始执行" << endl;
。。。。
//cout << "子线程opertator()执行完毕" << endl;
cout << "m_i1的值为:" << m_i << endl;
cout << "m_i2的值为:" << m_i << endl;
cout << "m_i3的值为:" << m_i << endl;
cout << "m_i4的值为:" << m_i << endl;
cout << "m_i5的值为:" << m_i << endl;
cout << "m_i6的值为:" << m_i << endl;
}
};
使用join()运行结果:
TA构造函数执行
TA的拷贝构造函数被执行
TA的析构函数被执行
主线程执行完毕1
TA的析构函数被执行
可以看见,在执行子线程时,TA执行了一次拷贝构造函数,证实了上面的说法。
第一次析构是线程中复制的对象析构,第二次是主线程析构对象。
lambda表达式创建线程
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
auto mylamthread = [] {
cout << "lambda子线程开始执行了" << endl;
};
thread mytobj4(mylamthread);
mytobj4.join();
cout << "主线程执行完毕1 " << endl;
return 0;
}
执行结果:
lambda子线程开始执行了
主线程执行完毕1
线程传参详解,detach()的注意事项,成员函数做线程函数
传递临时对象作为线程参数 (1)
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
void myprint(const int& i, char* pmybuf)
{
cout << i << endl;
cout << pmybuf << endl;
return;
}
int main()
{
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test!";
thread mytboj(myprint, mvar, mybuf);
mytboj.join();
cout << "主线程执行完毕 " << endl;
return 0;
}
运行结果:
1
this is a test!
主线程执行完毕
要避免的陷阱
对于传入参数i
上述程序看似没有问题,若是将 mytboj.join();
改为 mytboj.detach();
由于传入参数是一个引用,所以需要debug看一下变量的地址,实际操作下来:mvar 的地址 = 0x00c8fe8c ; mvary地址 = 0x00c8fe8c ,地址相同。i的地址 = 0x00f31314 ;可以看到,i的地址和mvar、mvary的地址不同,虽然传入是一个引用,但是实际上是复制了值。不会出现主线程执行完毕后释放i子线程不能调用的问题。
对于传入参数mybuf:
根据上述操作,找到地址:mybuf = 0x003af768 ;pmybuf = 0x003af768 ;
说明pmybuf使用时可能已经被系统回收。所以,指针再detach时一定会出现问题。
所以字符串怎么能够传入线程中?
传参改为:void myprint(const int& i, const string &pmybuf)
,两个地址很明显会不同。但是程序依然存在问题。
上面代码只是将char隐式转换为string。问题是:mybuf是在什么时候被隐式转换为string的?
事实上,存在mybuf都被回收了(main函数执行完)才开始将mybuf转换为string。所以这个方式是不稳定的。
稳定的方式如下:
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
//void myprint(const int& i, char* pmybuf)
void myprint(const int& i, const string &pmybuf)//这里不用引用会拷贝三次,造成内存浪费!
{
cout << i << endl;
cout << pmybuf.c_str() << endl;
return;
}
int main()
{
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test!";
thread mytboj(myprint, mvar, string(mybuf));//将mybuf直接转换为string,可以保证不出问题!
//mytboj.join();
mytboj.detach();
cout << "主线程执行完毕 " << endl;
Sleep(1000);
return 0;
所以在创建线程的同时构建临时对象的方法是可行的。
说这么多,建议不适用detach(). = =!
传递临时对象作为线程参数 (2)
线程ID的概念
id是一个数字,不管是主线程还是子线程,实际上都对应着一个数字,而且每一个线程对应的线程id不同。
线程id可以用C++标准库里的函数来获取。 std::this_thread::get_id()
来获取
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class A
{
public:
int m_i;
A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};
void myprint2(const A& pmybuf)
{
cout << "子线程myprint2的参数地址是:" << &pmybuf <<"threadId = " << std::this_thread::get_id() << endl;
}
int main()
{
cout << "主线程ID是:" << std::this_thread::get_id() << endl;
int mvar = 1;
thread mytobj(myprint2, mvar);
mytobj.join();
cout << "主线程执行完毕 " << endl;
return 0;
}
运行结果:
主线程ID是:15716
A::A(int a)的构造函数执行00B3F94CthreadId = 3224
子线程myprint2的参数地址是:00B3F94CthreadId = 3224
A::A()的析构函数执行00B3F94CthreadId = 3224
主线程执行完毕
可以看见,构造函数是在子线程3224中构造的,也是在子线程中析构的。如果不是join而是detach的话,变量已经在主线程中释放,子线程再构造时会出现不可预料的问题。
采用之前所说的直接转换对象,可以解决此问题,将上面 thread mytobj(myprint2, mvar);
改为 thread mytobj(myprint2, A(mvar));
之后,运行结果:
主线程ID是:20148
A::A(int a)的构造函数执行00EFFBD0threadId = 20148
A::A(const A)的拷贝构造函数执行01041370threadId = 20148
A::A()的析构函数执行00EFFBD0threadId = 20148
子线程myprint2的参数地址是:01041370threadId = 32688
A::A()的析构函数执行01041370threadId = 32688
主线程执行完毕
很明显可以看出传递对象是在主线程执行之前在主线程中构造20148出来。
传递类对象、智能指针作为线程参数
传递类对象作为线程参数
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class A
{
public:
mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};
void myprint2(const A& pmybuf)
{
pmybuf.m_i = 199;//修改该值不会影响到main函数
cout << "子线程myprint2的参数地址是:" << &pmybuf <<"threadId = " << std::this_thread::get_id() << endl;
}
int main()
{
cout << "主线程ID是:" << std::this_thread::get_id() << endl;
A myobj(10);
thread mytobj(myprint2,myobj);
mytobj.join();
return 0;
}
在这个程序中,修改了成员变量m_i的值,但是因为之前的特性,修改的只是线程中的拷贝构造函数所构造的对象成员,main函数中的成员并没有被修改。所以main函数中的m_i依然是10;
std::ref函数可以把引用的参数传递到线程中去。thread mytobj(myprint2,myobj);
改为 thread mytobj(myprint2,std::ref(myobj));
,并且此时可以去掉 void myprint2(const A& pmybuf)
中的const。就是说传递的是真的引用,而不是拷贝的对象。
传递智能指针作为线程参数
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class A
{
public:
mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};
void myprint2(unique_ptr<int> pzn)//智能指针
{
//pmybuf.m_i = 199;//修改该值不会影响到main函数
cout <<"threadId = " << std::this_thread::get_id() << endl;
}
int main()
{
unique_ptr<int> myp(new int(100));//独占智能指针
thread mytobj(myprint2,std::move(myp));//std::move()转移独占智能指针
mytobj.join();
return 0;
}
子线程中的pzn和主线程地址一样,所以用detach()会出现错误。(和前面原理一样)
用成员函数指针做线程函数
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class A
{
public:
mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
void thread_work(int num)
{
cout << "子线程执行" << this << "threadId = " << std::this_thread::get_id() << endl;
}
};
int main()
{
A myobj(10);
thread mytobj(&A::thread_work,myobj,15 );
mytobj.join();
return 0;
}
执行结果:
A::A(int a)的构造函数执行010FF9B4threadId = 12416
A::A(const A)的拷贝构造函数执行013915B4threadId = 12416
子线程执行013915B4threadId = 4888
A::A()的析构函数执行013915B4threadId = 4888
A::A()的析构函数执行010FF9B4threadId = 12416
拷贝构造函数执行,可以用detach(),节约空间使用std::ref(),或者&myobj的时候就可以真正的传递引用,不能用detach().
重载()的情况:
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class A
{
public:
mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
/* void thread_work(int num)
{
cout << "子线程执行" << this << "threadId = " << std::this_thread::get_id() << endl;
}*/
void operator()(int num)
{
cout << "子线程执行" << this << "threadId = " << std::this_thread::get_id() << endl;
}
};
void myprint2(unique_ptr<int> pzn)//智能指针
{
//pmybuf.m_i = 199;//修改该值不会影响到main函数
cout <<"threadId = " << std::this_thread::get_id() << endl;
}
int main()
{
//unique_ptr<int> myp(new int(100));//独占智能指针
//thread mytobj(myprint2,std::move(myp));//std::move()转移独占智能指针
A myobj(10);
thread mytobj(myobj,15 );
mytobj.join();
return 0;
}
运行结果:
A::A(int a)的构造函数执行012FFB64threadId = 33916
A::A(const A)的拷贝构造函数执行0154148CthreadId = 33916
子线程执行0154148CthreadId = 11580
A::A()的析构函数执行0154148CthreadId = 11580
A::A()的析构函数执行012FFB64threadId = 33916
依然要拷贝构造函数,见上一个知识点。