原文链接:Threads Lifetime

首先感谢作者的分享,然后本文是要用 Google 翻译加上个人的理解进行翻译的,部分内容可能不正确,仅供参考,推荐阅读原文


父母必须照顾孩子。这个简单的想法对线程生存期有很大的影响。以下程序启动一个线程,该线程显示其ID。

// threadWithoutJoin.cpp

#include <iostream>
#include <thread>

int main(){

std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});

}

但是程序运行会导致意外结果。

是什么原因呢?

join and detach

创建的线程 t 的生存期以其可调用单元结束。创建者有两种选择。第一:等待,直到其子项完成 (t.join())。第二:它将自己与孩子分离 (t.detach())。如果没有对线程调用 t.join()t.detach(),则具有可调用单元的线程 t(可以创建不带可调用单元的线程)是可连接的(joinable)。一个可连接的线程析构函数抛出 std::terminate 异常。因此,程序终止。因此,实际运行意外终止。

这个问题的解决方案很简单。通过调用 t.join(),程序将表现出应有的状态。

// threadWithJoin.cpp

#include <iostream>
#include <thread>

int main(){

std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});

t.join();

}

简短说明:detach 的挑战

当然,您可以在上面的程序中使用 t.detach() 代替 t.join()。线程 t 不再是可连接的,并且其析构函数未调用 std::terminate。似乎很糟糕,因为现在程序的行为是不确定的,因为无法确保对象 std::cout 的生存期。该程序的执行有点奇怪。

我将在下一篇文章中详细阐述这个问题。

移动线程

到目前为止,这还很容易。但这不一定是永远的。

复制线程(复制语义)是不可能的,只能移动(移动语义)它。如果线程被移动,要正确地处理线程的生存期将更加困难。

// threadMoved.cpp

#include <iostream>
#include <thread>
#include <utility>

int main(){

std::thread t([]{std::cout << std::this_thread::get_id();});
std::thread t2([]{std::cout << std::this_thread::get_id();});

t= std::move(t2);
t.join();
t2.join();

}

两个线程 t1 和 t2 只做一个简单的工作:打印它们的 ID。除此之外,线程 t2 将移至 t(t= std::move(t2))。最后,主线程等待子线程。可是等等。这与我的预期相去甚远:

怎么了?我们有两个问题:

  1. 通过移动线程 t2(转移所有权),t 获得一个新的可调用单元,并且其析构函数将被调用。所以 t 的析构函数调用 std::terminate,因为它仍然可以连接的。
  2. 线程 t2 没有关联的可调用单元。在没有可调用单元的线程上调用 join 会导致 std::system_error 异常。

我修复了两个错误。

// threadMovedFixed.cpp

#include <iostream>
#include <thread>
#include <utility>

int main(){

std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});
std::thread t2([]{std::cout << std::this_thread::get_id() << std::endl;});

t.join();
t= std::move(t2);
t.join();

std::cout << "\n";
std::cout << std::boolalpha << "t2.joinable(): " << t2.joinable() << std::endl;

}

如结果所示,线程 t2 不再是可连接的。

scoped_thread

如果不愿手动处理线程的生命周期,可以将 std::thread 封装在包装器类中。此类应自动在其析构函数中调用 join。当然,可以采用另一种方法来调用分离。但是你知道,分离存在一些问题。

Anthony Williams 创建了如此宝贵的类。他称其为 scoped_thread。在构造函数中,它检查线程是否可连接并最终在析构函数中将其连接。由于将拷贝构造函数和拷贝赋值运算符声明为 delete,因此·scoped_thread· 的对象不能复制也不能够通过赋值运算符赋值。

// scoped_thread.cpp

#include <iostream>
#include <thread>
#include <utility>


class scoped_thread{
std::thread t;
public:
explicit scoped_thread(std::thread t_): t(std::move(t_)){
if ( !t.joinable()) throw std::logic_error("No thread");
}
~scoped_thread(){
t.join();
}
scoped_thread(scoped_thread&)= delete;
scoped_thread& operator=(scoped_thread const &)= delete;
};

int main(){

scoped_thread t(std::thread([]{std::cout << std::this_thread::get_id() << std::endl;}));

}