目录
2.1.1 发起线程
1.有件事需要注意,当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解
析”(C++’s most vexing parse, 中文简介)。如果你传递了一个临时变量,而不是一个命名的变
量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
std::thread my_thread(background_task());
这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参
数并返回background_task对象的函数),返回一个 std::thread 对象的函数,而非启动了一个
线程。
使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。
- std::thread my_thread((background_task()));// 1
- std::thread my_thread{background_task()};// 2
2.函数已经结束,线程依旧访问局部变量:
- struct func
- {
- int& i;
- func(int& i_) : i(i_) {}
- void operator() ()
- {
- for (unsigned j=0 ; j<1000000 ; ++j)
- {
- do_something(i);
- // 1. 潜在访问隐患:悬空引用
- }
- }
- };
- void oops()
- {
- int some_local_state=0;
- func my_func(some_local_state);
- std::thread my_thread(my_func);
- my_thread.detach();
- }
- // 2. 不等待线程结束
- // 3. 新线程可能还在运行
这个例子中,已经决定不等待线程结束(使用了detach() ② ),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量。如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;当然,这从来就不是一个好主意——这种情况发生时,错误并不明显,会使多线程更容易出错。
处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。
此外,可以通过join()函数来确保线程在函数完成前结束。
清单2.1中,将my_thread.detach()替换my_thread.join(),就可以确保局部变量在线程完成后,才被销毁。
只能对一个线程使用一次join();一旦已经使用过join(),
std::thread对象就不能再次加入了,当对其使用joinable()时,将返回false。
- struct func; // 定义在代码2.1中
- void f()
- {
- int some_local_state=0;
- func my_func(some_local_state);
- std::thread t(my_func);
- try
- {
- do_something_in_current_thread();
- }
- catch(...)
- {
- t.join(); // 1
- throw;
- }
- t.join(); // 2
- }
一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),提供一个类,在析构函数中使用join()。如同下面代码。
- class thread_guard
- {
- std::thread& t;
- public:
- explicit thread_guard(std::thread& t_):
- t(t_)
- {}
- ~thread_guard()
- {
- if(t.joinable()) // 1
- {
- t.join(); // 2
- }
- }
- thread_guard(thread_guard const&)=delete; // 3
- thread_guard& operator=(thread_guard const&)=delete;
- };
-
- struct func; // 定义在代码2.1中
-
- void f()
- {
- int some_local_state=0;
- func my_func(some_local_state);
- std::thread t(my_func);
- thread_guard g(t);
- do_something_in_current_thread();
- } // 4
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束。
向可调用对象或函数传递参数很简单,只需要将这些参数作为 std::thread
构造函数的附加参数即可。需要注意的是,这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。
需要特别注意,指向动态变量的指针作为参数的情况:
- 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 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);
- }
相反的情形(期望传递一个非常量引用,但复制了整个对象)倒是不会出现,因为会出现编译错误。比如,尝试使用线程更新引用传递的数据结构:
- 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::ref
将参数转换成引用的形式。因此可将线程的调用改为以下形式:
std::thread t(update_data_for_widget,w,std::ref(data));
这样update_data_for_widget就会收到data的引用,而非data的拷贝副本,这样代码就能顺利的通过编译了。
也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:
- 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
构造函数的第三个参数就是成员函数的第一个参数,以此类推.
另一种有趣的情形是,提供的参数仅支持移动(move),当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()
进行显示移动。下面的代码展示了std::move
的用法,展示了std::move
是如何转移动态对象的所有权到线程中去的:
- void process_big_object(std::unique_ptr
) ; -
- std::unique_ptr
p(new big_object) ; - p->prepare_data(42);
- std::thread t(process_big_object,std::move(p));
C++标准线程库中和std::unique_ptr
在所属权上相似的类有好几种,std::thread
为其中之一。虽然,std::thread
不像std::unique_ptr
能占有动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个线程。线程的所有权可以在多个std::thread
实例中转移,这依赖于std::thread
实例的可移动且不可复制性。不可复制性表示在某一时间点,一个std::thread
实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。
- void some_function();
- void some_other_function();
- std::thread t1(some_function); // 1
- std::thread t2=std::move(t1); // 2
- t1=std::thread(some_other_function); // 3
- std::thread t3; // 4
- t3=std::move(t2); // 5
- t1=std::move(t3); // 6 赋值操作将使程序崩溃
代码2.8 生成多个线程,并等待它们完成运行:
- void do_work(unsigned id);
-
- void f()
- {
- std::vector
threads; - for (unsigned i = 0; i < 20; ++i)
- {
- threads.emplace_back(do_work,i); // 产生线程
- }
- for (auto& entry : threads) // 对每个线程调用 join()
- entry.join();
- }
代码2.9 并行版的std::accumulate:
- template<typename Iterator,typename T>
- struct accumulate_block
- {
- void operator()(Iterator first,Iterator last,T& result)
- {
- result=std::accumulate(first,last,result);
- }
- };
-
- template<typename Iterator,typename T>
- T parallel_accumulate(Iterator first,Iterator last,T init)
- {
- unsigned long const length=std::distance(first,last);
-
- if(!length) // 1
- return init;
-
- unsigned long const min_per_thread=25;
- unsigned long const max_threads=
- (length+min_per_thread-1)/min_per_thread; // 2
-
- unsigned long const hardware_threads=
- std::thread::hardware_concurrency();
-
- unsigned long const num_threads= // 3
- std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
-
- unsigned long const block_size=length/num_threads; // 4
-
- std::vector
results(num_threads) ; - std::vector
threads(num_threads-1) ; // 5 -
- Iterator block_start=first;
- for(unsigned long i=0; i < (num_threads-1); ++i)
- {
- Iterator block_end=block_start;
- std::advance(block_end,block_size); // 6
- threads[i]=std::thread( // 7
- accumulate_block
(), - block_start,block_end,std::ref(results[i]));
- block_start=block_end; // 8
- }
- accumulate_block
()( - block_start,last,results[num_threads-1]); // 9
-
- for (auto& entry : threads)
- entry.join(); // 10
-
- return std::accumulate(results.begin(),results.end(),init); // 11
- }
因为不能直接从一个线程中返回值,所以需要传递results容器的引用到线程中去。另一个办法,通过地址来获取线程执行的结果(第4章中,我们将使用future完成这种方案)。
线程标识为std::thread::id
类型,可以通过两种方式进行检索。第一种,可以通过调用std::thread
对象的成员函数get_id()
来直接获取。如果std::thread
对象没有与任何执行线程相关联,get_id()
将返回std::thread::type
默认构造值,这个值表示“无线程”。第二种,当前线程中调用std::this_thread::get_id()
(这个函数定义在
头文件中)也可以获得线程标识。
std::thread::id
实例常用作检测线程是否需要进行一些操作。比如:当用线程来分割一项工作(如代码2.9),主线程可能要做一些与其他线程不同的工作,启动其他线程前,可以通过std::this_thread::get_id()
得到自己的线程ID。每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。
- std::thread::id master_thread;
- void some_core_part_of_algorithm()
- {
- if(std::this_thread::get_id()==master_thread)
- {
- do_master_thread_work();
- }
- do_common_work();
- }
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);
}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);
}