1. 启动线程
线程是通过构造std::thread对象开始的,对该对象指定了线程上要运行的任务,在最简单的情况下,该任务仅仅是一个普普通通的返回void而且不接受参数的函数,这个函数在自己的线程上运行,直到返回,然后线程停止。但从另一个极端看,该任务可能是一个接收额外参数的函数对象,放它运行时,会执行一系列由某种消息机制所指定的相互独立的操作,并且只有当线程再次通过某种消息机制接收到信号才会停止。无论线程将要做什么或是从哪里启动,都需要构造std::thread对象。
#include<thread>
void do_some_work()
std::thread my_thread(do_some_work);
也可以将一个带有函数调用操作符的实例(如仿函数)传递给std::thread的构造函数来进行替代。
class background_task
{
public:
void operator() ()const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
还可以使用lambda表达式。
std::thread my_thread([](){
do_something();
});
一旦线程启动,你需要显式的决定等待完成还是让它自行运行,如果你在std::thread对象被销毁前还未做决定,那么你的程序会被终止(std::thread的析构函数会调用std::terminate())。因此在异常存在的情况下,确保线程正确地结合或者分离是当务之急。如果你不等待线程完成,那么你需要确保通过该线程访问的数据是有效的,直到线程完成为止。在对象销毁后还访问它是未定义行为。
如:
struct func
{
int& i;
func(int& i_):i(i_){}
void operator() ()
{
for(unsigned int j=0;j<1000000;++j)
{
do_something(i);
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); //不等待线程完成
//新的线程仍在运行
}
在这种情况下,当oops退出时线程还在运行,但是存储在栈中的局部变量随着函数的退出被释放,此时线程中仍在访问该变量,所以会发生未定义行为。此时可在代码中加入my_thread.join()解决。使用join()需要注意一个问题,为了避免应用程序在引发异常的时候被终止,需要注意在存在异常的时候调用join(),以避免意外的生命周期问题。
struct fun; //详见上图代码
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(....)
{
t.join(); //异常中断条件下
throw;
}
t.join(); //正常退出条件下
}
使用try/catch块很啰嗦,而且很容易将作用域弄luan,这里可以使用资源获取即初始化(RAII)惯用方法,并提供一个类,在它的析构函数中进行join()。
class thread_guard
{
std::thread &t;
public:
explicit thread_guard(std::thread& t_):t(t_)
{}
~thread_guard()
{
if(t.joinable())
{
t.join();
}
}
thread_guard(thread_guard const&)=delete;
thread_guard& operator= (thread_guard const&)=delete;//将拷贝构造函数和拷贝运算符标记为delete,防止对象被复制或赋值。
}
struct func;
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
当代码执行到f末尾时,局部对象会按照构造函数的逆序被销毁。因此,thread_guard对象g首先被销毁,并且析构函数中t.join()被结合,即使函数因为do_something_in_current_thread引发异常而退出的情况下也会发生。如果无需等待线程完成,可以通过分离来避免这个异常安全问题。这打破了线程与std::thread对象联系并确保当std::thread对象被销毁时std::terminate()不会被调用,即使线程仍在后台运行。
2. 传递参数给线程函数
传递参数给可调用对象或函数,基本上就是简单的将额外的参数传递给std::thread的构造函数,但重要的是参数会以默认的方式被复制到内部的存储空间,在那里新创建的执行线程可以访问它们,即便函数中相应参数期待着引用。
void f(int i,std::string const & s);
std::thread t(f,3,"hello");
这里创建一个新的与t相关联的执行线程,称为f(3,”hello”)。注意即使f接收std::string作为第二个参数时,字符串字面值仅在新线程的上下文中才作为char const*传送,并转换为std::string。尤其重要的是当提供的参数是一个自动变量的指针时。
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
如果oops在buffer在转化成std::string之前退出会发生未定义行为,因此解决之道是直接在std::thread的构造函数中传入std::string(buffer);
3.转移线程所有权。
c++标准库有许多拥有资源的类型,如std::ifstream和std::unique_ptr,都是可移动的而非可复制的,而且std::thread也是其中之一,都将拷贝构造函数和拷贝运算符写入私有,或者声明为delete。一个特定执行的线程所有权可以在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,t1与some_other_function()相关联,t2没有关联的线程,t3与运行的some_function()关联。
最后一次移动⑥将运行some_function()的线程的所以权转回给t1,但t1已经有一个相关联的线程,所以会调用std::terminate()来终止程序。