线程管理——向线程函数传递参数

前言

 
在上一节中,所有的入口函数均为无参函数,若某个入口函数存在形参,则需要向线程函数传递参数。这一行为并不复杂,仅仅需要在函数名后加上参数即可,例如:

1
2
void do_some_work(int); 
std::thread my_thread(do_some_work,5);

复制与引用

复制

在执行参数传递时需要注意的是,在不加以说明的情况下,所传参数必然以拷贝的形式传入线程独立内存中,即使在入口函数中形参是一个引用。举例而言:

1
2
void f(int i, std::string const& s); 
std::thread t(f, 3, "hello");

f需要的是一个string对象,而我们传入的是一个字面值常量,即const char *,因此这里首先完成了字面值向string对象的转化,并将该string对象拷贝至t的独立存储空间,并在其后完成引用绑定。

问题实例

当指向动态变量的指针作为参数传递给线程时需要特别注意:

1
2
3
4
5
6
7
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是一个指向本地char数组的指针。如果oops函数在线程对象t构造时意外退出(buffer转换成std::string对象之前),那么依赖buffer隐式转为string的愿景将无法实现。解决方案是在std::thread构造函数中显式地执行转换操作,确保转换发生在构造之前
1
2
3
4
5
6
7
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)); //免悬垂指针
t.detach();
}


引用

有时我们确实需要将变量以引用,而非复制的形式传入线程,比如我们需要处理一个被线程更新过的数据:

1
2
3
4
5
6
7
8
void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w) {
widget_data data;
std::thread t(update_data_for_widget,w,data);
display_status();
t.join();
process_widget_data(data);
}

在上述写法中,线程结束后更新完毕的data将会被销毁,process_widget_data处理的永远是最初的原始数据。
解决方案是采用std::ref将参数转为引用即可,此时update_data_for_widget将接收到data的引用,而非是data的拷贝的引用,如

1
std::thread t(update_data_for_widget,w,std::ref(data));

以成员函数作为入口函数

 
我们可以认为,成员函数本身也是一种普通函数,其特殊之处在于其第一个形参是this指针。因此,如下写法也不难理解:

1
2
3
4
5
6
7
class X { 
public:
void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);


移动

 
入口函数同样支持移动传入的变量。具体而言,临时变量总是以移动形式传入,而命名变量则需要显式地使用std::move:

1
2
3
4
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));

thread的移动性

std::thread类似于unique_ptr,也是一种move only object。一个std::thread持有一个执行线程,执行线程的所有权可以在多个std::thread间转移。std::thread的不可复制性保证了在同一时间点,一个std::thread仅可持有一个执行线程,而其移动性则保证了程序撰写者可以决定哪个std::thread对象可以拥有执行线程的所有权。