2.谁等谁
线程不能自己等自己。当我们说线程“不能自己等自己”时,这里的“自己”指的是实际线程,而不是C++代码中std::thread对象。
完整的表达应该是“不能调用当前线程对应的线程对象的join()方法”。
为了更好地理解“线程对象”与“线程对象对应的真实线程”之间的关系,我们把“在父线程中定义线程对象,创建市级线程,再通过线程对象调用join()方法,以便等待实际线程执行完毕”这一过程,画张图表示:
从图中可以看出,定义trd变量之时,所处的环境是父线程;调用trd.join()的也是父线程。显然,trd这个线程对象没有在它所对应的实际线程中调用join()方法。
“花样作死”
先看花样:
花样玩法是:为了创建trd对象,我们为它提供一个Lambda表达式作为它的构造入参,但这个Lambda表达式的实现,又需要依赖于“捕获”到trd变量。概念上听着显示“循环”依赖的谬论,技术上实现到是不难:因为Lambda实际捕获的“&trd”,只是一个地址。新线程会输出trd的线程ID,并断言它就是当前线程的ID,这当然正确。
接着开始“作死”,将trd.join()从父线程处挪入子线程的执行体:
一直以来举的多线程例子,都是父线程定义一个线程栈对象,从而创建子线程,接着子线程做点事,同时父线程做点事,最后父线程调用子线程的join()方法完成线程汇合。甚至这其中的“父线程”基本就是主线程,并且基本就是在main()函数中开展一切。
实际项目中情况往往会复杂一些,有时候会以new的方式创建堆中的std::trhread对象,或者采用shared_ptr <thread> 等智能指针管理,而指针非常方便传递,很可能被传递到对象实际对应的线程中去。
【小提示】:std::thread析构函数为何不尝试调用join()
std::thread类被设计为在析构过程中不去尝试调用join(),一个原因当然是避免在调用join()这件事上掺和一把,更乱了程序员;另一个更主要的原因是,join()(汇合)操作不是严格意义上的资源回收,不应将它归到RAII的设计范畴里。
当我们在A线程中定义新线程对象,并产生新线程B,我们就称A是B的父线程,B是A的子线程。
不过,子线程并不一定需要由父线程汇合。比如,让A线程(主线程)创建B线程、后者再创建C线程,但最终B、C线程都在主线程中汇合,如图所示:
在复杂需求下,将一些线程都交个某个固定线程(几乎就是主线程)同一管理及归并这样的设计不不罕见,但一定要清楚,B线程创建的C线程却交给A线程归并,显然又多了一处线程间的数据同步,发生“时空错乱”问题的机会大为增长。
12行,代码定义了一个全局变量 thread_vector 用于存放线程对象的裸指针(更好的做法是使用shared_ptr)。
17行,A线程(主线程)创建B线程对象,
20行B线程又创建C线程对象指针,
25行,将它加入到thread_vector中,
28行,最后循环汇合并释放该容器内的线程。
这种做法很常见,稍作封装,就可以实现将所有需要在主线程做善后的线程,都加入该容器,最终全部由主线程处理。
但上面的代码有个大问题:28行有极大的概率要比025行先执行,执行效果如下,可见25行还未执行,容器还是空的。
解决方案如下:
添加29行代码,28行是土办法
改进后运行效果:
【课堂作业】:改进主线程管理其他线程的例程
(1)解决主线程遍历容器时,容器还是空的问题
(2)将“thread * ”改为使用 shared_ptr <thread>;
(3)B的目标过程执行内容,改成循环创建10个C线程对象,cout改为使用COutWithMutx,输出内容需增加每个线程循环创建时的时序;
运行效果:这说明,45行,创建了10个C线程,这10个C线程,谁先创建完,不一定
C++语言经常被用于实现后台服务程序,这类程序往往1年365天不能下线(停止运行),程序的时间线近乎永恒。此时,线程相关的“时空错乱”问题往往变得更加隐秘,不易触发;另一方面,在主线程或某一专门线程中管理当前进程中的所有线程(或至少是由我们一手创建的线程)这一设计,在进程的监控维护、调优排错等方面,实用性更强了。
初学者还是老老实实按“父线程创建子线程,父线程负责汇合子线程”的思路写代码
如果真的不想管子线程,我们很快要讲到一个正招:线程“detach(剥离)”方法。该方法让线程对象和它代表的实际线程挥手一别,相忘江湖。当然如果就是想定义一个线程对象之后就不再理它,还有个“一不做二不休”的歪招。