启动线程
-
使用C++线程库启动线程,可以归结为构造std::thread对象。
void do_some_work();
std::thread my_thread(do_some_work);
-
std::thread可以用可调用类型构造,将带有函数调用符类型的实例传入std::thread类中,替换默认的构造函数。
class background_task{
public:
void operator()() const{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
- 提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致,否则得到的结果会与我们的期望不同。
-
如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
std::thread my_thread(backgroung_task());
-
使用在前面命名函数对象的方式,或使用多组括号,或使用新统一的初始化语法,可以避免这个问题
std::thread my_thread( (backgroung_task()) );
std::thread my_thread{ backgroung_task() };
-
使用lambda表达式也能避免这个问题。允许使用一个可以捕获局部变量的局部函数
std::thread my_thread([]{
do_something();
do_something_else();
});
- 启动线程之后:等待线程结束(加入式) / 让其自主运行(分离式)
必须在std::thread对象销毁之前做决定,否则你的程序将会终止(std::thread的析构函数会调用std::terminate() )
- 如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性
这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。
处理方法:将数据复制到线程中,而非复制到共享数据中,对于对象中包含的指针和引用还需谨慎
- 只能对一个线程使用一次join(),一旦使用过join(), std::thread 对象就不能再次汇入了。当对其使用joinable()时,将返回false。
- 避免应用被抛出的异常所终止。通常,在无异常的情况下使用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。
- 调用 std::thread 成员函数detach()来分离一个线程。之后,相应的 std::thread 对象就与实际执行的线程无关了,并且这个线程也无法汇入。
- 当 std::thread 对象使用t.joinable()返回的是true,就可以使用t.detach()。
- 不仅可以向 std::thread 构造函数传递函数名,还可以传递函数所需的参数(实参)。当然,也有其他方法可以完成这项功能,比如:使用带有数据的成员函数,代替需要传参的普通函数。
struct func;
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();
}
向线程函数传递参数
简单的传递
简单的传递,就在函数名称后面进行填写。
void f(int i;std::string const& s);
std::thread t(f,3,"hello");
默认的传值方式是值拷贝,thread会用新的地址来接收变量的值,即使函数接受的参数是引用也无效。而且,指向动态变量的指针作为参数传递时,指针指向本地变量,可能在转化的时候崩溃。
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; //1
sprintf(buffer,"%i","some_param");
std::thread t(f,3,buffer); //2
t.detach();
}
void f(int i,std::string const& s);
void not_oops(int some_param){
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer));//使用std::string避免悬垂指针
t.detach();
}
这种情况下,buffer2是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线
程中2。并且,函数有很有可能会在字面值转化成std::string对象之前崩溃(oops),从而导致一些未定义的行为。并且想要依赖隐式转换将字面值转换为函数期待的std::string对象,但因std::thread的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。
解决方案就是在传递到std::thread构造函数之前就将字面值转化为std::string对象:
还可能遇到相反的情况:期望传递一个引用,但整个对象被复制了。当线程更新一个引用传
递的数据结构时,这种情况就可能发生,比如:
//期望传递一个引用,但整个对象被复制了
void update_data_for_widget(widget_id w,widget_data& data); //1
void oops_again(widget_id w){
widget_data data;
std::thread t(update_data_for_widget,w,data); //2
//改为
//std::thread t(update_data_for_widget,w,std::ref(data));
display_status();
t.join();
process_widget_data(data); //3
}
虽然update_data_for_widget1的第二个参数期待传入一个引用,但是std::thread的构造函数2并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。当线程调用update_data_for_widget函数时,传递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。因此,当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,且process_widget_data将会接收到没有修改的data变量3
如果需要传递,需要使用std::ref()函数
std::thread t(update_data_for_widget,w,std::ref(data));
在这之后,update_data_for_widget就会接收到一个data变量的引用,而非一个data变量拷贝的引用。
使用类的成员函数作为线程函数
class X{
public:
void do_lengthy_work();
}
X my_x;
std::thread t(&X::do_lengthy_work(),&my_x); //1
class X{
public:
void do_lengthy_work();
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work,&my_x.num);
这段代码中,新线程将my_x.do_length_work作为线程函数;my_x的地址作为指向对象提供给函数,也可以为成员函数提供参数,std::thread第三个参数就是成员函数的第一个参数,以此类推。
但是提供的成员函数的参数可以移动,但不能拷贝。当原对象是一个临时对象,自动进行移动操作,但当原对象是命名变量,那么转移的时候就需要使用std::move进行显示移动。下面展示的std::move如何转移一个动态对象到一个线程中去:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));