如代码2.4所示, 向可调用对象或函数传递参数很简单, 只需要将这些参数作为std::thread构造函数的附加函数即可。这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代码创建了一个调用(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]; //(1)
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // (2)
t.detach();
}
buffer(1)是一个指针变量, 指向局部变量, 然后此局部变量通过buffer传递到新线程中(2)。此时, 函数oops可能会在buffer转换成std::string之前结束, 从而导致未定义的行为。 因为, 无法保证隐式转换的操作和std::thread构造函数的拷贝操作的顺序, 有可能std::thread的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到std::thread构造函数之前, 就将字面值转化为std::string:
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();
}
相反的情形(期望传递一个非常量引用, 但复制了整个对象)倒是不会出现, 因为会出现编译错误。 比如, 尝试使用线程更新引用传递的数据结构:
void update_data_for_widget(widget_id w, widget_data& data); // (1)
void oops_again(widget_id w)
{
widget_data data;
std::thread t(updata_data_for_widget, w, data); // (2)
display_status();
t.join();
process_widget_data(data);
}
虽然updata_data_for_widget(1)的第二个参数期待传入一个引用, 但std::thread的构造函数(2)并不知晓, 构造函数无视函数参数类型, 盲目地拷贝已提供的变量。 不过, 内部代码会将拷贝的参数以右值的方式进行传递, 这是为了哪些支持移动的类型, 而后会尝试以右值为实参调用updata_data_for_widget。 但因为函数期望的是一个非常量引用作为参数(而非右值), 所以会在编译时出错。 对于熟悉std::bing的开发者来说, 问题的解决方法很简单:可以使用std::ref将参数转换成引用的形式。 因此可将线程的调用改为以下形式:
std::thread t(update_data_for_widget, w, std::ref(data));
这样update_data_for_widget就会收到data的引用, 而非data的拷贝副本, 这样代码就能顺利的通过编译了。
如果熟悉std::bind, 就应该不会对以上传参的语法感到陌生, 因为std::thread构造函数和std::bind的操作在标准库中以相同的机制进行定义。 比如, 你也可以传递一个成员函数指针作为线程函数, 并提供一个合适的对象指针作为第一个参数:
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x); // 1
另一种有趣的情形是, 提供的参数仅支持移动(move),不能拷贝。 "移动"是指原始对象中的数据所有权转移给另一个对象, 从而这些数据就不再原始对象中保存(类似于剪切)。 std::unique_ptr就是这样一种类型, 这种类型为动态分配的对象提供内存自动管理机制(类似垃圾回收机制)。 同一时间内, 只允许一个std::unique_ptr实例指向一个对象, 并且当这个实例销毁时, 指向的对象也将被删除。 移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权后, 就会留下一个空指针。 使用移动操作可以将对象转换成函数可接受的实参类型, 或满足函数返回值类型要求。 当原对象是临时变量时, 则自动进行移动操作, 但当原对象是一个命名变量, 移动的时候就需啊哟使用std::move()进行显示移动。下面的代码展示了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(jprocess_big_object, std::move(p));
通过在std::thread构造函数中执行std::move§, big_object对象的所有权首先被转移到新创建线程的内部存储中, 之后再传递给process_big_object函数。
C++标准线程库中和std::unique_ptr在所属权上相似的类有好几种, std::thread为其中之一。 虽然, std::thread不像std::unique_ptr能占有动态对象的所有权, 但是它能占用其他资源: 每个实例都负责管理一个线程。 线程的所有权可以在多个std::thread实例中转移, 这依赖于std::thread实例的可移动且不可复制性。 不可复制性表示某一时间点, 一个std::thread实例只能关联一个执行线程。 可移动性是的开发者可以自己决定, 哪个实例拥有线程实际执行的所有权。