基本使用样例
这里使用一个基本的使用模版来说明thread的使用方式。
class background_task{
public:
void operator()(){
cout << "call background task in operator ()" << endl;
}
};
void do_on_background(){
cout << "call do_on_background" << endl;
}
int main(){
// 1.
thread t1(do_on_background);
t1.join();
// 2.
background_task bt;
thread t2(bt);
t2.join();
return 0;
}
/** output
call do_on_background
call background task in operator ()
*/
clang编译直接运行就行,linux下使用g++编译需要指定
-lpthread
。
上述代码的输出是确定的,不会出现不同的输出。基本的套路如下:
- 构建thread对象;
- 定义一个函数,作为thread构造函数的参数传入,可以是普通的对象或者是函数类的对象;
- 调用join函数,让主线程等子线程执行完再继续执行,保证了执行顺序;
在这个过程中,do_on_background等函数会被复制到线程的存储空间中,这些函数的执行和调用都在线程的内存空间中进行。
thread对象构造时的问题
在构造thread过程中会存在一个问题,对于普通函数do_on_background不存在,但是对于函数对象来说,会出现most vexing parse,具体来说是这样的:
我们以为下述代码的含义是:构建一个background_task的对象,并用这个临时的函数对象初始化thread对象:
thread t2(background_task());
但是,C++编译器是这么理解的:
声明了一个函数t2,返回thread对象,t2函数的参数是一个函数指针,该函数指针本质为:
background_task(*b)()
即返回值为background_task对象,且无参数的函数指针。使用这个写法来声明thread对象时,会有如下的错误出现:
直接告诉我们,这种写法被认为是函数声明,还告诉我们一种解决方法:
加个()
thread t2((background_task()));
还可以使用list initialization
thread t2{background_task()};
使用lambda表达式也是可以的;
thread t2([]() { cout << "call lambda expression" << endl;});
thread对象析构时的问题
当构造完成thread对象后,必须调用join或者detach来执行,否则会报错,其原因在于创建的线程有两个状态,如果thread对象析构时,为nonjoinable时,则会直接终止,如文章所述。
如果没有使用join或者detach时,当main程序结束之后,线程对象被析构之前,如果发现线程对象是joinable时,就会直接调用std::terminate结束线程的运行,如下:
When a thread object goes out of scope and it is in joinable state, the program is terminated.
join还是detach?
两者之间有什么区别吗?
join
Blocks the current thread until the thread identified by *this finishes its execution.
在上述样例代码中,join表示main线程等待t1和t2等线程的结束,才会继续执行。
该函数调用后,一定能保证线程的输出是确定的,不一定,只能保证创建子线程的线程与父线程之间有等待关系,多个兄弟线程之间不保证,如上代码改为下述代码就不保证输出是确定的。
thread t1(do_on_background); background_task bt; thread t2(bt); t1.join(); t2.join();
对于
join
的调用,一个线程只能使用一次,可以通过joinable
来判断线程是否允许join
,奇怪的是这个函数也可以用来判断是否允许detach
。在main中调用join,其实另外一个作用是,清理线程创建使用的资源,避免资源泄露。
detach
Separates the thread of execution from the thread object, allowing execution to continue independently. Any allocated resources will be freed once the thread exits.
该函数表示main对应的线程不管t1h和t2等线程的执行,它们可以独立地执行,因此也不会等待它们,这样最终输出的结果也是不确定的。
当线程执行结束后,其获得资源也会被释放。当main对应的线程结束后,detach的线程也不一定执行完成。当调用detach之后,在main函数中声明的线程对象与实际线程的执行没有关系,也无法进行管理了。
对象lifetime的影响
在单线程程序中,对于变量的访问,受到了其生命周期的影响,例如,当block scope结束后,其中的变量会被销毁,就不能进行访问了。但是,这种情况也很简单,因为只要保证单线程访问时,变量没有被销毁即可,这种条件,对于编程人员,可能很明显地发现来避免。
但是在多线程程序中,需要保证线程在访问变量时,变量仍然存在,此时,编程人员很难去发现,保证变量与线程的生命周期重叠。对于线程的启动,我们当前有join和detach来介入线程的实际运行方式。
join
void do_on_background(int a){ cout << a << ": call do_on_background" << endl; } int main(){ int a = 0; thread t(do_on_background, a); t.join(); return 0; } /** output: 0: call do_on_background */
这里的输出一定是确定的,因为t.join()保证在main线程停止,等待t线程结束运行,此时变量a一直存在,不会销毁。
detach
void do_on_background(int a){ cout << a << ": call do_on_background" << endl; } int main(){ int a = 0; thread t(do_on_background, ref(a)); t.detach(); return 0; }
在上述代码中线程t中可就会访问不到变量a,因为当main线程结束,a被销毁,其引用也就无意义了。但是这种问题并不容易发现。
针对这种问题,一般有两种解决方法:
- 使用join等待,确保局部变量在线程执行完才销毁;
- 将传递的数据进行复制,而不是引用,这样数据就在main和t线程中存在两份,分别对应个各自线程的生命周期。
在线程之间,尤其是指针和引用等数据要注意使用和共享。
资源泄露:join的位置在哪?
在前面,我们已经发现,join的位置不同,会导致输出的结果变得确定、或者不确定。另外join还会回收资源,如果join未执行,则会导致资源泄露,常见的导致join未执行的行为包括:
- 提前return;
- 原始线程出现异常被抛出;
如下:
thread t(do_on_background, 1);
throw invalid_argument("error");
t.join();
上述代码不会输出“call do_on_background”,因为有异常抛出,main非正常退出。当抛出异常后,程序直接调用std::terminate,不会执行到t.join,因此线程对象t的资源没有被释放。
如果在捕捉到相关的异常后,也可以调用join,这时也能保证线程的资源被回收,如下:
thread t(do_on_background, 1);
try {
throw invalid_argument("error");
}
catch(const exception &e) {
t.join();
}
t.join();
输出如下:
还可以使用RAII来解决这种问题:
class thread_guard
{
private:
std::thread &t;
public:
explicit thread_guard(std::thread &t_) : t(t_) {}
~thread_guard()
{
if (t.joinable())
{
t.join();
}
}
thread_guard(thread_guard const &) = delete;
thread_guard &operator=(thread_guard const &) = delete;
};
void do_on_background(int a)
{
cout << a << ": call do_on_background" << endl;
}
int main()
{
int a = 0;
thread t(do_on_background, a);
thread_guard g(t);
cout << "main over" << endl;
}
上述使用RAII来管理线程资源,当main函数退出时,对象g会被析构,然后就会调用join,使得线程对象t的资源被正确释放。但是,注意上述代码的输出是不确定的,这也是join位置造成的影响。
参考资料
- C++ Concurrency in action - 2.1 - Basic thread management