2.1 线程管理的基础
每个程序至少有一个线程:执行main()
函数的线程,其余线程有其各自的入口函数。线程与原始线程同时运行。
2.1.1 启动线程
使用C++线程库启动线程,可以归结为构造std::thread
对象:
void do_some_work();
std::thread my_thread(do_some_work);
std::thread
可以用可调用(callable)类型构造,将代用函数调用符类型的实例传入std::thread
类中,替换默认的构造函数。如果提供的是一个函数对象,那么会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。
HINT:当把函数对象传入到线程构造函数中,需要避免编译器将其解析为函数声明,如下:
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f); //可以!
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
std::thread my_thread(background_task{}); // 3
std::thread my_thread([]{
do_something();
do_something_else();
}); //4
启动了线程之后,需要明确是要等待线程结束,还是让其自主运行。如果std::thread
对象销毁之前还没有做出决定,程序就会终止:std::thread
的析构函数会调用std::terminate()
。
如果不等待线程(如使用成员函数detach()
),就必须保证在线程结束之前,访问的数据具有有效性。很可能发生在线程还没结束,函数已经退出的时候:
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()
函数,这就会访问已经销毁的变量。
处理办法如下:将数据复制到线程中,而非复制到共享数据(此处是指主线程和新线程都可访问的部分)中。如果使用一个可调用的对象作为线程函数,那么这个对象就会复制到线程中,而后原始对象就会立刻销毁,但对于对象中包含的指针和引用需要谨慎,使用一个能访问局部变量的函数去创建线程不可行!还可以使用**join**
来确保线程在函数完成前结束。
2.1.2 等待线程完成
join()
是简单粗暴的等待线程完成或不等待,如果需要对等待中的线程有更灵活的控制时,比如看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时),就要使用其他机制来完成:条件变量和future
。
调用join()
还清理了线程相关的存储部分,这样**std::thread**
就不再与已经完成的线程有任何关联。
2.1.3 特殊情况下的等待
如果想分离一个线程,可以在线程启动后,直接调用detach()
进行分离;如果想等待线程,需要斟酌调用join()
的位置:如果在线程运行之后产生的异常在join()
之前抛出,就意味着这次join()
会被跳过。所以最好在异常处理过程中调用join()
。
如果需要确保线程在函数之前结束——查看是否因为线程函数使用了局部变量的引用或指针,以及其他原因——而后再确定一下程序可能会退出的途径,无论正常与否,可以使用RAII
机制,并提供一个类,在析构函数中使用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
当线程执行到④时,局部对象就要被逆序销毁,因此thread_guard
对象g
是第一个被销毁的,此时②线程在析构函数中被加入到的原始线程中。这样即使do_something_in_current_thread
抛出异常,这个销毁依旧会发生。
在**thread_guard**
的析构函数的测试中,首先判断线程是否已加入①,如果没有会调用**join()**
②进行加入。这很重要,因为**join()**
只能对给定的对象调用一次,所以对给已加入的线程再次进行加入操作时,将会导致错误。
拷贝构造函数和拷贝赋值操作被标记为=delete
③,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给**thread_guard**
对象赋值的操作都会引发一个编译错误。
2.1.4 后台运行线程
使用detach()
会让线程在后台运行,即主线程不能与之产生交互,也没有任何std::thread
对象能引用它,它运行于后台。
通常称分离线程为守护线程,UNIX守护线程指没有任何用户接口,并在后台运行的线程。
2.2 向线程传递参数
向std::thread
构造函数中的可调用对象或函数传递一个参数,直接从第二个参数依次传入即可。默认情况下,参数都是拷贝到线程独立内存中,即使参数是引用形式。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
但是,当指向动态变量的指针作为参数传递给线程:
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);
t.detach();
}
在这种情况,buffer
是一个指向本地变量的指针变量,然后本地变量通过buffer
传递到新线程中,并且函数可能会在字面值转化成std::string
对象之前崩溃,从而导致未定义行为。想要依赖隐式转换将字面值转换为函数期待的std::string
对象,但因std::thread
的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。
解决方案就是在传递给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::ref()
将参数转换成引用形式,这样就真正绑定到了实参上:
std::thread t(update_data_for_widget,w,std::ref(data));
此外,std::thread
构造函数可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:
class X{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);
也可以为成员函数提供参数,从第三个参数开始依次排开即可。
提供的参数可以移动,但不能拷贝。移动操作可以将对象转换成可接受的类型,e.g.函数参数或函数返回的类型。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要显式使用std::move()
进行显示移动。**std::thread**
所有权可以在多个实例中互相转移,因为这些实例是可移动(movable)且不可复制(aren’t copyable)。在同一时间点,就能保证只关联一个执行线程;同时,也允许程序员能在不同的对象之间转移所有权。(通过std::move
)
2.3 转移线程所有权
C++标准库中有很多资源占有(resource-owning)类型,比如std::ifstream
,std::unique_ptr
还有std::thread
都是可移动(movable),但不可拷贝(cpoyable)。
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 赋值操作将使程序崩溃
当显式使用std::move()
创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了;执行some_function
的函数现在与t2关联。
最后一个移动操作,将some_function
线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()
终止程序继续运行。终止操作将调用std::thread
的析构函数,销毁所有对象。
std::thread
支持移动,就意味着线程的所有权可以在函数外进行转移,比如函数返回std::thread
对象:
std::thread f(){
void some_function();
return std::thread(some_function);
}
std::thread g(){
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
当所有权可以在函数内部传递,就允许std::thread
实例可作为参数进行传递:
#include <iostream>
#include <thread>
using namespace std;
void f(std::thread t) {
std::cout << "f() yes!\n";
if (t.joinable())
t.join();
}
void func() {
cout << "func() yes!\n";
}
void g() {
f(std::thread(func));
std::thread t(func);
f(std::move(t));
}
int main(void) {
g();
return 0;
}
std::thread
支持移动的好处在于可以创建**thread_guard**
类的实例,并且拥有其线程的所有权。当thread_guard
对象所持有的线程已经被引用,移动操作就可以避免很多不必要的麻烦,即当某个对象转移了线程的所有权后,它就不能对线程进行进入或分离。
对于std::thread
对象的容器,如果这个容器是移动敏感的(比如std::vector
),那么移动操作同样适用于这些容器:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void func(unsigned id) {
cout << id<<" ";
}
int main(void) {
std::vector<std::thread> threads;
for (unsigned i = 0; i < 20; ++i) {
threads.emplace_back(func, i);
}
//这里的引用时不可缺少的,不可以值传递
for (auto& i : threads)
i.join();
return 0;
}
2.4 运行时决定线程数量
std::thread::hardware_cocurrency()
将返回能同时并发在一个程序中的线程数量。e.g. 多核系统中,返回值可以是CPU核心数。以下是 amd 3500u,四核八线程处理器的运行结果:
2.5 识别线程
线程标识类型是std::thread::id
,可以通过两种方式获得:
- 调用
std::thread
成员函数get_id()
直接获取。如果该线程对象没有与任何执行线程相关联,该函数返回std::thread::type
默认构造值,表示没有线程; - 在当前线程中调用
std::this_thread::get_id()
也可获得本线程的标识。
std::thread::id
对象可以自由拷贝和对比,例如可以作为容器键、可以排序