C++ 标准库之移交线程归属权解析

前景知识

理解线程归属权的移交方法,需要了解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;
	}


};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泽箬酱咧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值