传递参数给线程函数
传递参数给可调用对象或函数,基本上就是简单地将额外的参数传递给 std:: thread 构造函数。但重要的是,参数会以默认的方式被复制(copied)到内部存储空间,在那里新创建的执行线程可以访问它们,即便函数中的相应参数期着引用。这里有一个简单的例子。
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();
}
在这种情况下,正是局部变量 buffer❶的指针被传递给新线程❷,还有一个重要的时机,即函数oops 会在缓冲在新线程上被转换为std::string之前退出,从而导致未定义的行为。解决之道是在将缓冲传递给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();
}
在这种情况下,问题就出在你依赖从缓冲的指针到函数所期望的std::string对象的隐式转换,因为std::thread构造函数原样复制了所提供的值,并未转换为期望的参数类型。
也有可能得到相反的情况,对象被复制,而你想要的是引用。这可能发生在当线程正在更新一个通过引用传递来的数据结构时,例如:
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); //❸
}
尽管update_data_for_widget❶希望通过引用传递第二个参数,std::thread的构造函数❷却并不知道,它无视函数所期望的类型,并且盲目地复制了所提供的值。当它调用update_data_for_widget 时,它最后将传递data在内部的副本的引用而非对data自身的引用。于是,当线程完成时,随着所提供参数的内部副本的销毁,这些改动都将被舍弃,将会传递一个未改变的data❸,而非正确更新的版本给process_widget_data。对于熟悉std::bind的人来说,解决方案也是显而易见的,你需要用std::ref来包装确实需要被引用的参数。在这种情况下,如果你将对线程的调用改为
std::thread t(update_data_for_widget, w, std::ref(data));
那么update_data_for_widget将被正确地传人data的引用,而非data**副本(copy)**的引用。
如果你熟悉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); //❶
这段代码将在新线程上调用my_x.do_lengthy_work(),因为my_x的地址是作为对象指针◎提供的。你也可以提供参数给这样的成员函数调用:std::thread构造函数的第三个参数将作为成员函数的第一个参数等等。
提供参数的另一个有趣的场景是,这里的参数不能被复制但只能被移动(moved);一个对象内保存的数据被转移到另一个对象,使原来的对象变成“空壳”。这种类型的一个例子是 std::unique_ptr,它提供了动态分配对象的自动内存管理。只有一个std::unique_ptr实例可以在某一时刻指向一个给定的对象,当该实例被销毁时,其指向的对象将被删除。**移动构造函数(move constructor)和移动赋值运算符(move assignment operator)**允许一个对象的所有权在std::unique_ptr实例之间进行转移。这种转移给源对象留下一个NULL指针。这种值的移动使得该类型的对象作为函数的参数被接受或从函数返回值。在源对象是临时的场合,这种移动是自动的,但在源是一个命名值的地方,此转移必须直接通过调用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));
通过在std: :thread构造函数中指定 std::move(p), big_object
的所有权先被转移进新创建的线程的内部存储中,然后进入process_big_object。
标准线程库中的一些类表现出与std::unique_ptr相同的所有权语义,std::thread就是其中之一。虽然std::thread实例并不拥有与std::unique_ptr同样方式的动态对象,但他们却拥有资源,每一个实例负责管理一个执行线程。这种所有权可以在实例之间进行转移,因为std::thread的实例是可移动的
(movable),即使他们不是可复制的(copyable)。这确保了在允许程序员选择在对象之间转换所有权的时候,在任意时刻只有一个对象与某个特定的执行线程相关联。