Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

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 call other 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::refstd::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实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。

评论