2.3 转移所有权
假设通过新线程返回的所有权去调用一个需要后台启动线程的函数,并需要在函数中转移线程的所有权。这些操作都要等待线程结束才能进行,并且需要线程的所有权能够进行转移。
这就是将移动操作引入std::thread的原因,C++标准库中有很多资源占有(resource-owning)类型,比如std::ifstream,std::unique_ptr还有std::thread都是可移动,但不可复制。这说明执行线程的所有权可以在std::thread实例中移动,下面将展示一个例子。例子中,创建了两个执行线程,并在std::thread实例之间(t1,t2和t3)转移所有权:
void some_function();void some_other_function();std::thread t1(some_function); // 1std::thread t2=std::move(t1); // 2t1=std::thread(some_other_function); // 3std::thread t3; // 4t3=std::move(t2); // 5t1=std::move(t3); // 6 赋值操作将使程序崩溃
首先,新线程与t1相关联①。当显式使用std::move()创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行some_function的函数线程与t2关联。
然后,临时std::thread对象相关的线程启动了③。为什么不显式调用std::move()转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用。
t3使用默认构造方式创建④,没有与任何线程进行关联。调用std::move()将t2关联线程的所有权转移到t3中⑤。因为t2是一个命名对象,需要显式的调用std::move()。移动操作⑤完成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行some_function的线程相关联。
最后一个移动操作,将some_function线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序继续运行。这样做(不抛出异常,std::terminate()是noexcept函数)是为了保证与std::thread的析构函数的行为一致。2.1.1节中,需要在线程对象析构前,显式的等待线程完成,或者分离它,进行赋值时也需要满足这些条件(说明:不能通过赋新值给std::thread对象的方式来”丢弃”一个线程)。
std::thread支持移动,线程的所有权可以在函数外进行转移,就如下面程序一样。
代码2.5 函数返回std::thread对象
std::thread f(){void some_function();return std::thread(some_function);}std::thread g(){void some_other_function(int);std::thread t(some_other_function,42);return t;}
当所有权可以在函数内部传递,就允许std::thread实例作为参数进行传递,代码如下:
void f(std::thread t);void g(){void some_function();f(std::thread(some_function));std::thread t(some_function);f(std::move(t));}
std::thread支持移动可以创建thread_guard类的实例(定义见代码2.3),并且拥有线程所有权。当引用thread_guard对象所持有的线程时,移动操作就可以避免很多不必要的麻烦。当某个对象转移了线程的所有权,就不能对线程进行汇入或分离。为了确保线程在程序退出前完成,定义了scoped_thread类。现在,我们来看一下这个类型:
代码2.6 scoped_thread的用法
class scoped_thread{std::thread t;public:explicit scoped_thread(std::thread t_): // 1t(std::move(t_)){if(!t.joinable()) // 2throw std::logic_error(“No thread”);}~scoped_thread(){t.join(); // 3}scoped_thread(scoped_thread const&)=delete;scoped_thread& operator=(scoped_thread const&)=delete;};struct func; // 定义在代码2.1中void f(){int some_local_state;scoped_thread t(std::thread(func(some_local_state))); // 4do_something_in_current_thread();} // 5
与代码2.3相似,不过新线程会直接传递到scoped_thread中④,而非创建一个独立变量。当主线程到达f()末尾时⑤,scoped_thread对象就会销毁,然后在析构函数中完成汇入③。代码2.3中的thread_guard类,需要在析构中检查线程是否“可汇入”。这里把检查放在了构造函数中②,并且当线程不可汇入时抛出异常。
C++17标准给出一个建议,就是添加一个joining_thread的类型,这个类型与std::thread类似,不同是的添加了析构函数,就类似于scoped_thread。委员会成员们对此并没有达成统一共识,所以这个类没有添加入C++17标准中(C++20仍旧对这种方式进行探讨,不过名称为std::jthread),这个类实现起来也不是很困难。
代码2.7 joining_thread类的实现
class joining_thread{std::thread t;public:joining_thread() noexcept=default;template<typename Callable,typename ... Args>explicit joining_thread(Callable&& func,Args&& ... args):t(std::forward<Callable>(func),std::forward<Args>(args)...){}explicit joining_thread(std::thread t_) noexcept:t(std::move(t_)){}joining_thread(joining_thread&& other) noexcept:t(std::move(other.t)){}joining_thread& operator=(joining_thread&& other) noexcept{if(joinable()){join();}t = std::move(other.t);return *this;}joining_thread& operator=(std::thread other) noexcept{if(joinable())join();t=std::move(other);return *this;}~joining_thread() noexcept{if(joinable())join();}void swap(joining_thread& other) noexcept{t.swap(other.t);}std::thread::id get_id() const noexcept{return t.get_id();}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;}};
std::thread中对移动语义的支持,也适用于使用std::thread的移动敏感(move-aware)容器(比如,std::vector<>)。了解这些后,就可以量产了一些线程,并且等待它们结束,代码如下所示。
代码2.8 量产线程,等待它们结束
void do_work(unsigned id);void f(){std::vector<std::thread> threads;for (unsigned i = 0; i < 20; ++i){threads.emplace_back(do_work,i); // 产生线程}for (auto& entry : threads) // 对每个线程调用 join()entry.join();}
我们有时需要线程去分割一个算法的工作总量,所以在算法结束的之前,所有的线程必须结束。代码2.8中线程所做的工作都是独立的,并且结果仅会受到共享数据的影响。如果f()有返回值,这个返回值就依赖于线程得到的结果。写入返回值之前,程序会检查使用共享数据的线程是否终止。结果在不同线程中转移的方案,会在第4章中再次讨论。
将std::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,而是把它们当做一个组。创建一组线程(数量在运行时确定),而非代码2.8那样创建固定数量的线程。
