目录
2.1 线程的基本管控
2.1.2 等待线程完成
2.1.3 异常情况下的等待
2.1.4 后台运行线程
2.2 向线程函数传递参数
2.3 移交线程归属权
2.4 在运行时选择线程数量
2.5 识别线程
参考:https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019/blob/master/content/chapter2/2.1-chinese.md
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
struct accumulate_block
{
void operator()(Iterator first,Iterator last,T& result)
{
result=std::accumulate(first,last,result);
}
};
template
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);
}