基本使用样例
这里使用一个基本的使用模版来说明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