转移线程的所有权
假设你想要编写一个函数,它创建一个在后台运行的线程,但是向调用函数回传新线程的所有权,而非等待其完成,又或者你想要反过来做,创建一个线程,并将所有权传递给要等待它完成的函数。在任意一种情况下,你都需要将所有权从一个地方转移到另一个地方。
这里就是std::thread支持移动的由来。正如在上一节所描述的,在C++标准库里许多拥有资源的类型,如std::ifstream 和 std::unique_ptr是可移动的(movable),而非可复制的(copyable),并且std::thread就是其中之一。这意味着一个特定执行线程的所有权可以在std::thread实例之间移动,如同接下来的例子。该示例展示了创建两个执行线程,以及在三个std::thread实例t1、t2和t3之间对那些线程的所有权进行转移。
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构建完成时所有权被转移给t2,通过调用std::move()来显式地转移所有权❷。此刻,t1不再拥有相关联的执行线程,运行some_function的线程现在与t2相关联。
然后,启动一个新的线程并与一个临时的std::thread对象相关联❸。接下来将所有权转移到t1中,是不需要调用std::move()来显式移动所有权的,因为此处所有者是一个临时对象——从临时对象中进行移动是自动和隐式的。
t3是默认构造的❹,这意味着它的创建没有任何相关联的执行线程。当前与t2相关联的线程的所有权转移到t3❺,再次通过显式调用std::move(),因为t2是一个命名对象。在所有这些移动之后,t1与运行some_other_function 的线程相关联,t2没有相关联的线程,t3与运行some_function的线程相关联。
最后一次移动❻将运行 some_function的线程的所有权转回给t1。但是在这种情况下t1已经有了一个相关联的线程(运行着some_other_function),所以会调用std::terminate()来终止程序。这样做是为了与std::thread的析构函数保持一致。你必须在析构前显式地等待线程完成或是分离,这同样适用于赋值:你不能仅仅通过向管理一个线程的std::thread对象赋值一个新的值来“舍弃”一个线程。
std::thread支持移动意味着所有权可以很容易地从一个函数中被转移出,如清单2.5所示。
//清单2.5 从函数中返回std::thread
#include <thread>void some_function()
{}void some_other_function(int)
{}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;
}int main()
{std::thread t1 = f();t1.join();std::thread t2 = g();t2.join();
}
同样地,如果要把所有权转移到函数中,它只能以值的形式接受std::thread的实例作为其中一个参数,如下所示。
void f(std::thread t);void g()
{void some_other_function();f(std::thread(some_function));std::thread t(some_function);f(std::move(t));
}
std::thread支持移动的好处之一,就是你可以建立在清单2.3中thread_guard 类的基础上,同时使它实际上获得线程的所有权。这可以避免thread_guard 对象在引用它的线程结束后继续存在所造成的不良影响,同时也意味着一旦所有权转移到了该对象,那么其他对象都不可以结合或分离该线程。因为这主要是为了确保在退出一个作用城之前线程都已完成,我把这个类称为scoped_thread。其实现如清单2.6所示,同时附带一个简单的示例。
#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 const&)=delete;scoped_thread& operator=(scoped_thread const&)=delete;
};void do_something(int& i)
{++i;
}struct func
{int& i;func(int& i_):i(i_){}void operator()(){for(unsigned j=0;j<1000000;++j){do_something(i);}}
};void do_something_in_current_thread()
{}void f()
{int some_local_state;scoped_thread t(std::thread(func(some_local_state))); //❹do_something_in_current_thread();
} //❺int main()
{f();
}
这个例子与清单2.3类似,但是新线程被直接传递到scoped_thread❹,而不是为它创建一个单独的命名变量。当初始线程到达 f❺ 的结尾时,scoped_thread对象被销毁,然后结合❸提供给构造函数❶的线程。使用清单2.3中的thread_guard类,析构函数必须检查线程是不是仍然可结合,你可以在构造函数中❷来做,如果不是则引发异常。
std::thread对移动的支持同样考虑了std::thread对象的容器,如果那些容器是移动感知的(如更新后的std::vector<>
)。这意味着你可以编写像清单2.7中的代码,生成一批线程,然后等待它们完成。
#include <vector>
#include <thread>
#include <algorithm>
#include <functional>void do_work(unsigned id)
{}void f()
{std::vector<std::thread> threads;for(unsigned i=0; i<20; ++i){threads.push_back(std::thread(do_work, i)); //生成线程}std::for_each(threads.begin(), threads.end(),std::mem_fn(&std::thread::join)); //轮流在每个线程上调用join()
}int main()
{f();
}
如果线程是被用来细分某种算法的工作,这往往正是所需的。在返回调用者之前,所有线程必须全都完成。当然,清单2.7的简单结构意味着由线程所做的工作是自包含的,同时它们操作的结果纯粹是共享数据的副作用。如果f()向调用者返回一个依赖于这些线程的操作结果的值,那么正如所写的这样,该返回值就得通过检查线程终止后的共享数据来决定。
将std::thread对象放到std::vector中是线程迈向自动管理的一步。与其为那些线程创建独立的变量并直接与之结合,不如将它们视为群组。你可以进一步创建在运行时确定的动态数量的线程,更进一步地利用这一步,而不是如清单2.7中的那样创建固定的数量。