第二章 线程管控
主要内容:
- 启动线程,并通过几种方式为新线程指定运行代码
- 等待线程完成和分离线程并运行
- 唯一识别一个线程
2.1 线程的基本管控
? main函数其本声就是一个线程,在其中又可以启动别的线程和设置其对应的函数入口。
2.1.1 发起线程
? 不管线程要执行的任务是复杂还是简单,其最终都要落实到标准库
std::thread my_thread((background_task()));
下面我写了一个验证程序来验证作者所说的这一种情况:
class background_task
{
public:
//函数转化操作符,将类转为函数对象
void operator()() const
{
cout<<"background_task's function convert"<<endl;
}
};
void func_inside_mythread()
{
cout<<"func_inside_mythread"<<endl;
}
//一个返回background_task对象的函数
background_task do_something()
{
cout<<"do somthing inside background_task"<<endl;
return background_task();
}
//这里便是引发编译器歧义的申明,这里既可以声明为thread对象的创建也可以是一个参数为返回background_task函数指针的函数
thread my_thread(background_task(*p)());
//如下便是对于上面t1的函数定义
thread my_thread(background_task(*p)())
{
(*p)();
cout<<"I am a function which get function pointer background_task"<<endl;
return thread(func_inside_mythread);
}
int main()
{
thread mt = my_thread(do_something);
mt.join();
return 0;
}
? 上面的t1就是引发编译器歧义的地方,也就是作者所举例说明的情况,下面来看一下执行结果:
? 可以发现编译器把my_thread看作是了一个函数定义,但是实际上这里我传入的参数是故意给了一个具名的返回background_tast对象的p函数指针,实际上还可以这么写:
int main()
{
//1
thread my_thread(background_task());
//2
background_task f;
thread my_thread(f);
return 0;
}
? main里的第一句,其实按照语法上来讲,这里的backgroun_task类已经做过了函数类型转化的操作了,在这里正常时可以解释成我定义了一个thread线程对象,他接收可调用对象background_task函数,但是实际上通过vscode自带的提示器,将鼠标移动上去以后可以看见它仍然提示这是一个函数声明:
? 那如何解决上述问题呢?其实就是书上说的C11以后引入了新式的统一初始化语法,也叫列表初始化,像下面这样写就不存在编译器把这一行解释成函数的情况了(或者还可以直接传入lambda表达式做临时函数变量也能解决问题):
int main()
{
thread my_thread{background_task()};
my_thread.join();
return 0;
?
? 接下来作者说到了线程的分离和汇合,这里要总结一个概念:如果什么都不设置,thread对象析构时将自动终止线程程序,如果分离,就算thread对象已经彻底析构了,线程程序还在自己继续跑着。
? 这也接下来引发了第二个问题,即如果新线程的函数上持有指向主线程的变量或者数据的指针或引用时,但主线程运行退出后,新线程还没结束时,这个时候再访问那些指针的时候就是非法访问了,书配套代码如下:
#include <thread>
void do_something(int& i)
{
++i;
}
struct func
{
int& i;
func(int& i_):i(i_){}
void operator()()
{
//for(unsigned j=0;j<1000000;++j)
//这里为了复现非法访问的情况改成了无限循环
while(1)
{
do_something(i);
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach();
}
int main()
{
oops();
}
? 这里要引发的问题就在于,主线程detach以后结束了,自然局部变量得到释放,但是新增的线程仍然还在跑着,因为传的是引用类型,所以这个时候再去访问就是非法访问了,以下是我的运行结果,估计是Linux内部的什么机制,主线程一旦退出子线程随即也退出的场景我没有复现出来:
? 可以看见在gdb中切换到子进程以后输入c命令让程序自动运行,主线程775856先退出,随后子进程立刻也跟着退出了。
? 解决上述问题的方法作者也给了出来,主要是两点,一是让线程函数完全自含(self-contained),另一种是使用thread的join函数,确保子进程在父进程之前退出,也就是汇合线程操作。如果想要更加精细化地控制线程等待,则要到后面讲条件变量和future的时候继续学习,一旦调用了join,则这个线程相关的任何存储空间都将被立即删除。
2.1.3 在出现异常的情况下等待
? 接下来说到了在出现异常状况下的join等待,主要的问题在于当新线程启动以后,如果有异常抛出,但是这个时候join在异常的后面,这样join就得不到执行了略过了,先来看一下代码清单2.2中不用try-catch的情况:
#include <thread>
#include <iostream>
using namespace std;
void do_something(int& i)
{
++i;
}
struct func
{
int& i;
func(int& i_):i(i_){}
void operator()()
{
for(unsigned j=0;j<1000000;++j)
{
do_somet