线程生命周期
原文链接:Threads Lifetime
首先感谢作者的分享,然后本文是要用 Google 翻译加上个人的理解进行翻译的,部分内容可能不正确,仅供参考,推荐阅读原文
父母必须照顾孩子。这个简单的想法对线程生存期有很大的影响。以下程序启动一个线程,该线程显示其ID。
// threadWithoutJoin.cpp |
但是程序运行会导致意外结果。
是什么原因呢?
join and detach
创建的线程 t 的生存期以其可调用单元结束。创建者有两种选择。第一:等待,直到其子项完成 (t.join()
)。第二:它将自己与孩子分离 (t.detach()
)。如果没有对线程调用 t.join()
或 t.detach()
,则具有可调用单元的线程 t(可以创建不带可调用单元的线程)是可连接的(joinable)。一个可连接的线程析构函数抛出 std::terminate
异常。因此,程序终止。因此,实际运行意外终止。
这个问题的解决方案很简单。通过调用 t.join()
,程序将表现出应有的状态。
// threadWithJoin.cpp |
简短说明:detach 的挑战
当然,您可以在上面的程序中使用 t.detach()
代替 t.join()
。线程 t 不再是可连接的,并且其析构函数未调用 std::terminate
。似乎很糟糕,因为现在程序的行为是不确定的,因为无法确保对象 std::cout
的生存期。该程序的执行有点奇怪。
我将在下一篇文章中详细阐述这个问题。
移动线程
到目前为止,这还很容易。但这不一定是永远的。
复制线程(复制语义)是不可能的,只能移动(移动语义)它。如果线程被移动,要正确地处理线程的生存期将更加困难。
// threadMoved.cpp |
两个线程 t1 和 t2 只做一个简单的工作:打印它们的 ID。除此之外,线程 t2 将移至 t(t= std::move(t2)
)。最后,主线程等待子线程。可是等等。这与我的预期相去甚远:
怎么了?我们有两个问题:
- 通过移动线程 t2(转移所有权),t 获得一个新的可调用单元,并且其析构函数将被调用。所以 t 的析构函数调用
std::terminate
,因为它仍然可以连接的。 - 线程 t2 没有关联的可调用单元。在没有可调用单元的线程上调用
join
会导致std::system_error
异常。
我修复了两个错误。
// threadMovedFixed.cpp |
如结果所示,线程 t2 不再是可连接的。
scoped_thread
如果不愿手动处理线程的生命周期,可以将 std::thread
封装在包装器类中。此类应自动在其析构函数中调用 join
。当然,可以采用另一种方法来调用分离。但是你知道,分离存在一些问题。
Anthony Williams 创建了如此宝贵的类。他称其为 scoped_thread
。在构造函数中,它检查线程是否可连接并最终在析构函数中将其连接。由于将拷贝构造函数和拷贝赋值运算符声明为 delete,因此·scoped_thread· 的对象不能复制也不能够通过赋值运算符赋值。
// scoped_thread.cpp |