2.2 传递参数:std::thread构造函数
规则
如代码2.4所示,向可调用对象或函数传递参数很简单,只需要将这些参数作为 std::thread
构造函数的附加参数即可。需要注意的是,这些参数会**拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式**,拷贝操作也会执行。
事实上,以附加参数传入的参数,其在线程函数中的类型只能是值传递或者const引用传递(const引用可以是因为可以const引用右值),否则会编译错误
cppreference thread() noexcept; (1) (since C++11) thread( thread&& other ) noexcept; 移动构造函数,1.参数类型thread&&右值引用类型暗示实现是移动语义(即浅拷贝+源指针置空) 2.noexcept则一旦在其上下文中抛出异常,异常处理机制将直接调用std::terminate() (2) (since C++11) template< class Function, class… Args > explicit thread( Function&& f, Args&&… args ); 构造函数 (3) (since C++11) thread( const thread& ) = delete; 拷贝构造函数,不允许编译器生成 (4) (since C++11) Constructs new thread object.
\1) Creates new thread object which does not represent a thread.
\2) Move constructor. Constructs the thread object to represent the thread of execution that was represented by
other
. After this callother
no longer represents a thread of execution.\3) Creates new
std::thread
object and associates it with a thread of execution. The new thread of execution starts executing /*INVOKE*/(std::move(f_copy), std::move(args_copy)…), where
/*INVOKE*/ performs the
*INVOKE*
operation specified in Callable
f_copy
is an object of type std::decay::type and constructed from std::forward (f) , and
如果Function类型为左值引用类型,则f_copy为左值,否则f_copy类型为右值
args_copy...
are objects of types std::decay::type… and constructed from std::forward (args)…. Constructions of these objects are executed in the context of the caller,
so that any exceptions thrown during evaluation and copying/moving of the arguments are thrown in the current thread, without starting the new thread. The program is ill-formed if any construction or the
*INVOKE*
operation is invalid.This constructor does not participate in overload resolution if std::decay
::type is the same type as thread
.The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.
\4) The copy constructor is deleted; threads are not copyable. No two
std::thread
objects may represent the same thread of execution.
来看一个例子:
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
替换隐式转换为显示转换
代码创建了一个调用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]; // 1
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // 2
t.detach();
}
buffer①是一个指针变量,指向局部变量,然后此局部变量通过buffer传递到新线程中②。此时,函数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(update_data_for_widget,w,data); // 2
display_status();
t.join();
process_widget_data(data);
}
虽然update_data_for_widget①的第二个参数期待传入一个引用,但std::thread
的构造函数②并不知晓,构造函数无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用update_data_for_widget。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。
使用std::ref传递引用
std::ref
和std::cref
事实上是模板函数,返回值是一个std::reference_wrapper
对象(该对象有一个数据成员:一个指针),而std::reference_wrapper
虽然是一个对象,可是他却能展现出和普通引用类似的效果
对于熟悉std::bind
的开发者来说,问题的解决办法很简单:可以使用std::ref
将参数转换成引用的形式。因此可将线程的调用改为以下形式:
std::thread t(update_data_for_widget,w,std::ref(data));
这样仍然是发生拷贝,拷贝的std::reference_wrapper
对象,这个对象满足函数引用参数类型的要求
这样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
这段代码中,新线程将会调用my_x.do_lengthy_work(),其中my_x的地址①作为对象指针提供给函数。也可以为成员函数提供参数:std::thread
构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。
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传递只能移动不能拷贝的类型
另一种有趣的情形是,提供的参数仅支持移动(move),不能拷贝。``std::unique_ptr`是这种类型
使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。
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::move将左值p强制类型转换为右值引用类型
C++标准线程库中和std::unique_ptr
在所属权上相似的类有好几种,std::thread
为其中之一。虽然,std::thread
不像std::unique_ptr
能占有动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个线程。线程的所有权可以在多个std::thread
实例中转移,这依赖于**std::thread
实例的可移动且不可复制性**。不可复制性表示在某一时间点,一个std::thread
实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。