前景知识
理解线程归属权的移交方法,需要了解std::move(右值转换)和std::forward(完美转发)相关知识,先介绍三个规则。
规则1(引用折叠规则):如果间接的创建一个引用的引用,则这些引用就会“折叠”。在所有情况下(除了一个例外),引用折叠成一个普通的左值引用类型。一种特殊情况下,引用会折叠成右值引用,即右值引用的右值引用, T&& &&。即
- X& &、X& &&、X&& &都折叠成X&
- X&& &&折叠为X&&
规则2(右值引用的特殊类型推断规则):当将一个左值传递给一个参数是右值引用的函数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用,如:
template<typename T>
void f(T&&);
int i = 42;
f(i)
上述的模板参数类型T将推断为int&类型,而非int。若将规则1和规则2结合起来,则意味着可以传递一个左值int i
给f,编译器将推断出T的类型为int&。再根据引用折叠规则 void f(int& &&)将推断为void f(int&),因此,f将被实例化为: void f<int&>(int&)。从上述两个规则可以得出结论:如果一个函数形参是一个指向模板类型的右值引用,则该参数可以被绑定到一个左值上。
规则3(static_cast转换):虽然不能隐式的将一个左值转换为右值引用,但是可以通过static_cast显示地将一个左值转换为一个右值。【C++11中为static_cast新增的转换功能】。标准库中move的定义如下:
template<typename T>
typename remove_reference<T>::type && move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
- std::move
move函数的参数T&&是一个指向模板类型参数的右值引用【规则2】,通过引用折叠,此参数可以和任何类型的实参匹配(传递左值时,折叠成T&,传递右值时折叠成T&&),因此move既可以传递一个左值,也可以传递一个右值。从move的定义可以看出,move自身除了做一些参数的推断之外,返回右值引用本质上还是靠static_cast<T&&>完成的。注意,std::move返回的永远是右值引用。
- std::forward
std::forward<T>()不仅可以保持左值或者右值不变,同时还可以保持const、lreference、rreference、validate等属性不变。
移交线程归属权
何为移交线程归属权?假设编写一个函数,功能是创建线程,并置于后台运行,但该函数本身不等待线程完结,而是将其归属权向上移交给函数的调用者;或相反地,读者想创建线程,遂将其归属权传入某个函数,由它负责等待该线程结束。两种操作都需要转移线程的归属权。
std::thread支持移动语义是移交线程归属权的前提,所谓移动语义,简单来说就是资源只能移动不可以拷贝。
std::thread 移交线程归属权实例解析:
void some_function();
void some_other_function();
std::thread t1(some_function); // -----①
std::thread t2 = std::move(t1); // -----②
t1 = std::thread(some_other_function); // -----③
std::thread t3; // -----④
t3 = std::move(t2); // -----⑤
t1 = std::move(t3); // -----⑥ 该赋值操作会终止整个程序
首先,我们启动新线程①,并使之关联t1。接着,构建t2,在其初始化过程中调用std::move(),将新线程的归属权显式地转移给t2[9]②。在②之前,t1关联着执行线程,some_function()函数在其上运行;及至②处,新线程关联的变换为t2。
然后,启动另一新线程③,它与一个std::thread类型的临时对象关联。新线程的归属权随即转移给t1。这里无须显式调用std::move(),因为新线程本来就由临时变量持有,而源自临时变量的移动操作会自动地隐式进行。
t3按默认方式构造④,换言之,在创建时,它并未关联任何执行线程。在⑤处,t2原本关联的线程的归属权会转移给t3,而t2是具名变量,故需再次显式调用std::move(),先将其转换为右值。经过这些转移,t1与运行some_other_function()的线程关联,t2没有关联线程,而t3与运行some_function()的线程关联。
在最后一次转移中⑥,运行some_function()的线程的归属权转移到t1,该线程最初由t1启动。但在转移之时,t1已经关联运行some_other_function()的线程。因此std::terminate()会被调用,终止整个程序。该调用在std::thread的析构函数中发生,目的是保持一致性。2.1.1节已解释过,在std::thread对象析构前,我们必须明确:是等待线程完成还是要与之分离。不然,便会导致关联的线程终结。赋值操作也有类似的原则:只要std::thread对象正管控着一个线程,就不能简单地向它赋新值,否则该线程会因此被遗弃。
std::thread支持移动操作的意义是,函数可以便捷地向外部转移线程的归属权。
一个更加完整的例子:
#pragma once
#include<thread>
class joining_thread
{
using this_type = joining_thread;
using this_thread = std::thread;
this_thread t;
public:
joining_thread() noexcept = default;
//模板函数,func可以是函数指针,类成员函数,函数对象或者lamda表达式
//std::forward 完美转发,保留参数的左右值,常量及其引用的属性
template<typename Callable,typename ... Args>
explicit joining_thread(Callable && func, Args&& ... args) :
t(std::forward<Callable>(func), std::forward<Args>(args)...)
{}
explicit joining_thread(this_thread t_) noexcept :t(std::move(t_)) {}
joining_thread (this_type && other) noexcept :t(std::move(other.t)){}
//析构函数
~joining_thread() noexcept
{
if (joinable())
{
join();
}
}
this_type & operator = (this_type && other) noexcept
{
if (joinable())
{
join();
}
t = std::move(other.t);
return *this;
}
this_type & operator = (this_thread other) noexcept
{
if (joinable())
{
join();
}
t = std::move(other);
return *this;
}
bool joinable() const noexcept
{
return t.joinable();
}
void join()
{
t.join();
}
void detach()
{
t.detach();
}
std::thread & as_thread() noexcept
{
return t;
}
const std::thread & as_thread() const noexcept
{
return t;
}
};