2.1 线程管理的基础

每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程同时运行

2.1.1 启动线程

使用C++线程库启动线程,可以归结为构造std::thread对象:

  1. void do_some_work();
  2. std::thread my_thread(do_some_work);

std::thread可以用可调用(callable)类型构造,将代用函数调用符类型的实例传入std::thread类中,替换默认的构造函数。如果提供的是一个函数对象,那么会复制到新线程的存储空间中函数对象的执行和调用都在线程的内存空间中进行
HINT:当把函数对象传入到线程构造函数中,需要避免编译器将其解析为函数声明,如下:

  1. class background_task
  2. {
  3. public:
  4. void operator()() const
  5. {
  6. do_something();
  7. do_something_else();
  8. }
  9. };
  10. background_task f;
  11. std::thread my_thread(f); //可以!
  12. std::thread my_thread(background_task()); //不可以!

第二种写法相当于声明了一个名为**my_thread**的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。
可以使用如下方法避免:

  1. std::thread my_thread( (background_task()) ); // 1
  2. std::thread my_thread{background_task()}; // 2
  3. std::thread my_thread(background_task{}); // 3
  4. std::thread my_thread([]{
  5. do_something();
  6. do_something_else();
  7. }); //4

启动了线程之后,需要明确是要等待线程结束,还是让其自主运行。如果std::thread对象销毁之前还没有做出决定,程序就会终止:std::thread的析构函数会调用std::terminate()

如果不等待线程(如使用成员函数detach()),就必须保证在线程结束之前,访问的数据具有有效性。很可能发生在线程还没结束,函数已经退出的时候

  1. struct func
  2. {
  3. int& i;
  4. func(int& i_) : i(i_) {}
  5. void operator() ()
  6. {
  7. for (unsigned j=0 ; j<1000000 ; ++j)
  8. {
  9. do_something(i); // 1. 潜在访问隐患:悬空引用
  10. }
  11. }
  12. };
  13. void oops()
  14. {
  15. int some_local_state=0;
  16. func my_func(some_local_state);
  17. std::thread my_thread(my_func);
  18. my_thread.detach(); // 2. 不等待线程结束
  19. } // 3. 新线程可能还在运行

本例中,已经决定不等待线程结束(调用detach()),所以当oops()函数执行完成时,新线程中的函数可能还在运行,他就会调用do_something()函数,这就会访问已经销毁的变量。
处理办法如下:将数据复制到线程中,而非复制到共享数据(此处是指主线程和新线程都可访问的部分)中。如果使用一个可调用的对象作为线程函数,那么这个对象就会复制到线程中,而后原始对象就会立刻销毁,但对于对象中包含的指针和引用需要谨慎,使用一个能访问局部变量的函数去创建线程不可行!还可以使用**join**来确保线程在函数完成前结束

2.1.2 等待线程完成

join()是简单粗暴的等待线程完成或不等待,如果需要对等待中的线程有更灵活的控制时,比如看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时),就要使用其他机制来完成:条件变量和future
调用join()清理了线程相关的存储部分,这样**std::thread**就不再与已经完成的线程有任何关联。

2.1.3 特殊情况下的等待

如果想分离一个线程,可以在线程启动后,直接调用detach()进行分离;如果想等待线程,需要斟酌调用join()的位置:如果在线程运行之后产生的异常在join()之前抛出,就意味着这次join()会被跳过。所以最好在异常处理过程中调用join()

如果需要确保线程在函数之前结束——查看是否因为线程函数使用了局部变量的引用或指针,以及其他原因——而后再确定一下程序可能会退出的途径,无论正常与否,可以使用RAII机制,并提供一个类,在析构函数中使用join()

  1. class thread_guard{
  2. std::thread& t;
  3. public:
  4. explicit thread_guard(std::thread& t_):t(t_){}
  5. ~thread_guard(){
  6. if(t.joinable()) // 1
  7. {
  8. t.join(); // 2
  9. }
  10. }
  11. thread_guard(thread_guard const&)=delete; // 3
  12. thread_guard& operator=(thread_guard const&)=delete;
  13. };
  14. struct func; // 定义在清单2.1中
  15. void f(){
  16. int some_local_state=0;
  17. func my_func(some_local_state);
  18. std::thread t(my_func);
  19. thread_guard g(t);
  20. do_something_in_current_thread();
  21. } // 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构造函数中的可调用对象或函数传递一个参数,直接从第二个参数依次传入即可。默认情况下,参数都是拷贝到线程独立内存中,即使参数是引用形式

  1. void f(int i, std::string const& s);
  2. std::thread t(f, 3, "hello");

但是,当指向动态变量的指针作为参数传递给线程:

  1. void f(int i, std::string const& s);
  2. void oops(int some_param){
  3. char buffer[1024]; //1
  4. sprintf(buffer, "%i", some_param);
  5. std::thread t(f, 3, buffer);
  6. t.detach();
  7. }

在这种情况,buffer是一个指向本地变量的指针变量,然后本地变量通过buffer传递到新线程中,并且函数可能会在字面值转化成std::string对象之前崩溃,从而导致未定义行为。想要依赖隐式转换将字面值转换为函数期待的std::string对象,但因std::thread的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值

解决方案就是在传递给std::thread构造函数之前就将字面值转化为std::string对象:

  1. void f(int i,std::string const& s);
  2. void not_oops(int some_param)
  3. {
  4. char buffer[1024];
  5. sprintf(buffer,"%i",some_param);
  6. std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针
  7. t.detach();
  8. }

如果例程形参是引用,而传给线程构造函数中直接将实参传递进去的话,实际上还是对实参进行了拷贝,而例程中的引用,绑定的是实参内部的一份拷贝,跟实参毫无关系。
可以使用std::ref()将参数转换成引用形式,这样就真正绑定到了实参上:

  1. std::thread t(update_data_for_widget,w,std::ref(data));

此外,std::thread构造函数可以传递一个成员函数指针作为线程函数并提供一个合适的对象指针作为第一个参数

  1. class X{
  2. public:
  3. void do_lengthy_work();
  4. };
  5. X my_x;
  6. 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)。

  1. void some_function();
  2. void some_other_function();
  3. std::thread t1(some_function); // 1
  4. std::thread t2=std::move(t1); // 2
  5. t1=std::thread(some_other_function); // 3
  6. std::thread t3; // 4
  7. t3=std::move(t2); // 5
  8. 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对象:

  1. std::thread f(){
  2. void some_function();
  3. return std::thread(some_function);
  4. }
  5. std::thread g(){
  6. void some_other_function(int);
  7. std::thread t(some_other_function,42);
  8. return t;
  9. }

当所有权可以在函数内部传递,就允许std::thread实例可作为参数进行传递:

  1. #include <iostream>
  2. #include <thread>
  3. using namespace std;
  4. void f(std::thread t) {
  5. std::cout << "f() yes!\n";
  6. if (t.joinable())
  7. t.join();
  8. }
  9. void func() {
  10. cout << "func() yes!\n";
  11. }
  12. void g() {
  13. f(std::thread(func));
  14. std::thread t(func);
  15. f(std::move(t));
  16. }
  17. int main(void) {
  18. g();
  19. return 0;
  20. }

std::thread支持移动的好处在于可以创建**thread_guard**类的实例,并且拥有其线程的所有权。当thread_guard对象所持有的线程已经被引用,移动操作就可以避免很多不必要的麻烦,即当某个对象转移了线程的所有权后,它就不能对线程进行进入或分离

对于std::thread对象的容器,如果这个容器是移动敏感的(比如std::vector),那么移动操作同样适用于这些容器:

  1. #include <iostream>
  2. #include <thread>
  3. #include <vector>
  4. using namespace std;
  5. void func(unsigned id) {
  6. cout << id<<" ";
  7. }
  8. int main(void) {
  9. std::vector<std::thread> threads;
  10. for (unsigned i = 0; i < 20; ++i) {
  11. threads.emplace_back(func, i);
  12. }
  13. //这里的引用时不可缺少的,不可以值传递
  14. for (auto& i : threads)
  15. i.join();
  16. return 0;
  17. }

2.4 运行时决定线程数量

std::thread::hardware_cocurrency()将返回能同时并发在一个程序中的线程数量。e.g. 多核系统中,返回值可以是CPU核心数。以下是 amd 3500u,四核八线程处理器的运行结果:
image.png

2.5 识别线程

线程标识类型是std::thread::id,可以通过两种方式获得:

  • 调用std::thread成员函数get_id()直接获取。如果该线程对象没有与任何执行线程相关联,该函数返回std::thread::type默认构造值,表示没有线程
  • 在当前线程中调用std::this_thread::get_id()也可获得本线程的标识

std::thread::id对象可以自由拷贝和对比,例如可以作为容器键、可以排序