C++11并发与多线程
0 引言
并发与多线程是目的与手段之间的关系。并发是指大于等于2的活动同时发生。并发对软件行业的影响见下文。
https://blog.csdn.net/hsutter/article/details/1435298
(1)线程:线程是用来执行代码的一条通路,一个新的线程代表一条新的通路。
(2)进程:运行起来的可执行程序,每个可执行文件运行起来,在操作系统的任务管理器中显示为一条进程。线程与进程的关系:每个进程都有一个唯一的主线程。
(3)主线程:用来执行main函数的线程叫做主线程,主线程与进程唇齿相依。
(4)软件并发:软件并发是指多个进程通过任务的快速切换,从呈现效果上来说展示为并发的一种状态。
(5)硬件并发:指多核处理器可以真的同时执行多个任务。
(6)并发的实现方式有两种:一种是多进程并发,一种是多线程并发。多进程并发要考虑通信。进程之间的通信方式有很多种,同一台电脑上分为:管道,文件,消息队列,共享内存等;不同电脑上可以采用socket通信技术。进程之间存在数据保护,进程之间的通信是一个复杂的事情。
(7)多线程并发:一个进程中的所有线程共享地址空间,通过全局变量等在线程之间传递,其开销远远小于多进程。因此,一般并发优先考虑使用多线程技术。
1 多线程编程
(1)自己创建的函数要从一个初始函数开始执行。
(2)join:加入和汇合的意思,一个thread对象执行join的效果是让主线程等待子线程执行完毕,再执行主线程。这也是传统的多线程程序推荐的方法,这样写出来的程序可以保证每个子线程都有有头有尾。
(3)detach与join是相对的,是一个特例,一个thread对象执行了detach命令后,主线程与子线程分离,主线程不必等待子线程执行完毕再执行,主线程也不必非得等待子线程结束。detach之后,子线程被c++运行时库接管了。detach会使子线程失去控制,使用有风险。一旦detach了,不能再join了,否则系统会报告异常。
(4)joinable: 判断线程对象是否可以join或者detach,一般可以join就可以detach,不能join就不能detach,所以没有detachable.
2 子线程初始函数中的临时对象传递问题
(1)普通函数中的临时对象传递问题
首先创建测试用例如下。 void MyPrint(const int & temp_i, char* temp_ch) { std::cout <<temp_i<< std::endl; std::cout << ch << std::endl; } void TestMyPrint() { int i = 10; int &ref_i = i; char ch[] = "This is a test!"; MyPrint(ref_i , ch); } 在运行此用例时,利用vs中的快速监视功能,观察变量的值,发现:
&i = 0x0021f9c0 {10}
&ref_i = 0x0021f9c0 {10}
&temp_i = 0x0021f9c0 {10}
可以得出结论:普通函数中,当形参为引用时,操作系统不会复制此变量。
ch = 0x002ff838 "This is a test!"
temp_ch = 0x002ff838 "This is a test!"
可以得出结论:普通函数中,当形参为指针时,操作系统不会复制此变量。
因此,采用引用和指针传递变量的效率会比创建其他变量要高,因为操作系统需要额外复制一份临时变量。
(2)子线程的初始函数中的临时对象传递问题
void MyPrint(const int & temp_i, char* temp_ch) { std::cout << temp_i << std::endl; std::cout << temp_ch << std::endl; } void TestTempObj() { int i = 10; int &ref_i = i; char ch[] = "This is a test!"; std::thread temp_thread(MyPrint, ref_i, ch); temp_thread.join(); }
在运行此用例时,利用vs中的快速监视功能,观察变量的值,发现:
&i = 0x0021f9b4 {10}
&ref_i = 0x0021f9b4 {10}
&temp_i = 0x002ab44c {10}
可以得出结论,主线程在创建子线程时,其初始函数中的引用符号失效,引用变量仍然会单独复制一份。
ch = 0x0021f990 "This is a test!"
temp_ch = 0x0021f990 "This is a test!"
可以得出结论,主线程在创建子线程时,其初始函数中的指针变量的值不变,不会单独复制一份。
由于这个原因,在创建子线程时需要特别小心传递指针类型的变量,很容易出错。另外,传递引用类型变量时,如果发生了类型转换,也有可能会出错。
(3)结论
在子线程的初始函数中,建议采用如下规范:
(a)若传递int这种简单类型的变量,建议采用值传递,降低风险;
(b)若传递的是类对象,避免隐式类型转换,因为存在如下情况:
class A{ int m_a; A(int a):m_a(a){ } ~A(); }; void Print(const A& a){ }
int main()
{ int a_int = 10; std::thread my_thread(Print, a_int); // 此处的a_int可以隐式转化为A my_thread.detach();
return 0;
}
在这种情况下,my_thread子线程与主线程分离,存在一种可能:即主线程已经执行完了,a_int对象已经被内存给释放了,但是子线程初始函数中的构造函数还未执行,使得该类型转换无法完成,程序出错。
(c)子线程的初始函数传递的参数类型为类对象时,必须使用引用,如果不用的话,函数将调用三次拷贝构造函数,造成严重的浪费。
(d)建议在操作子线程时,不使用detach,只是用join, 这样就不存在局部变量时效,导致对内存的非法引用问题。
(4)临时对象构造时机的捕捉
TA::TA(const int& i) : m_i(i) { std::cout << "TA(const int& i) 构造函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl; } TA::TA(const TA& a) { this->m_i = a.m_i; std::cout << "TA(const TA& a) 拷贝构造函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl; } TA::~TA() { std::cout << "~TA()析构函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl; }
(5)通过std::ref修饰子线程的初始函数中的参数对象,使得该值可以被修改;此时,编译器不再强行复制该引用值,而是直接采用引用的方式传递值,函数将可以在子线程中修改参数,并传回到主线程中。举例:
void ChangeValue(const TA& a) { a.m_i = 18993838; } void TestMyPrint1() { std::cout << "主线程的id号是: " << std::this_thread::get_id() << std::endl; int i = 19; TA a(19); std::thread temp_thread(ChangeValue, std::ref(a)); std::cout << a.m_i << std::endl; temp_thread.join(); }