1. 线程管理基础

1.1 启动线程

线程在std::thread对象创建(为线程指定任务)时启动。最简单的情况下,任务也会很简单,通常是无参数无返回的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。总之,使用C++线程库启动线程,可以归结为构造std::thread对象,三种基本的构造方式:

  • 指定启动函数

    1. #include <thread> //头文件
    2. void do_some_work();
    3. std::thread my_thread(do_some_work);//指定启动函数,线程启动
  • 同大多数C++标准库一样,std::thread可以通过带有函数操作符类型的实例,进行构造: ```cpp class background_task { public: void operator()() const //重载的函数操作符 { do_something(); do_something_else(); } };

background_task f; std::thread my_thread(f);

  1. - 将多个顺序函数简化为**lambda表达式**的类型:
  2. ```cpp
  3. std::thread my_thread([]{
  4. do_something();
  5. do_something_else();
  6. });

1.2 等待线程结束

  • 如果需要等待线程,相关的std::thread实例需要使用join(),就可以确保局部变量在线程完成后,才被销毁。只能对一个线程使用一次join();一旦已经使用过join(),std::thread对象就不能再次加入了,当对其使用joinable()时,将返回false。
  • 如果不需要等待线程结束,则需要使用了detach(),这种方式千万不要对其他线程和主线程的局部变量的指针或引用进行操作

Note:join()和detach()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这些,你需要使用其他机制来完成,比如条件变量,参考第4章同步和并发。

Note:C++20标准中提供了一个新的类**jthread,**jthread的析构函数可以自动的进行join操作,不需要像thread一样显示的调用。

1.3 RAII等待线程完成

使用资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用join()

  1. #include <iostream>
  2. #include <thread>
  3. using namespace std;
  4. class thread_guard
  5. {
  6. private:
  7. std::thread& t;
  8. public:
  9. explicit thread_guard(std::thread& t_) : t(t_) {}
  10. ~thread_guard()
  11. {
  12. if (t.joinable()) // 1,返回true,说明线程t还没有结束
  13. {
  14. t.join(); // 2,此时,主线程需要等待线程t结束,不然局部变量some_local_state会提前释放
  15. }
  16. }
  17. // 3 C++11新特性,不允许编译器自动生成赋值函数和拷贝构造函数
  18. thread_guard(thread_guard const&) = delete;
  19. thread_guard& operator=(thread_guard const&) = delete;
  20. };
  21. struct func
  22. {
  23. int& i;
  24. func(int& i_) : i(i_) {}
  25. void operator() ()
  26. {
  27. for (unsigned j = 0; j < 1000; ++j)
  28. {
  29. cout << "do_something(" << i << ");" << endl;
  30. }
  31. }
  32. };
  33. int main()
  34. {
  35. int some_local_state = 0;
  36. func my_func(some_local_state);
  37. std::thread t(my_func);
  38. thread_guard g(t);//交给guard类管理线程t的销毁
  39. cout << "do_something_in_current_thread();" << endl;
  40. return 0;
  41. } // 4

当线程执行到④处时,局部对象就要被逆序销毁了。因此,thread_guard对象g是第一个被销毁的,这时线程在析构函数中被加入②到原始线程中。即使do_something_in_current_thread抛出一个异常,这个销毁依旧会发生。

1.4 后台运行线程

使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。通常称分离线程为守护线程(daemon threads):

  • 没有任何显式的用户接口,并在后台运行的线程
  • 长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化
  • 分离线程的另一方面只能确定线程什么时候结束

2. 向线程函数传递参数

向可调用对象或函数传递参数很简单,只需要将这些参数作为 **std::thread** 构造函数的附加参数即可。但需要注意的是,在默认情况下,这些参数会被拷贝至新线程的独立内存空间中,以供新线程访问,并如同临时变量一样作为右值传递给可调用对象或函数。即使函数中的参数是引用的形式,拷贝操作也会执行。

2.1 字符串的传递

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

代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型。之后,在线程的上下文中完成字面值向std::string对象的转化。

特别需要注意的是,当指向动态变量的指针作为参数传递给线程的情况,代码如下:

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

这种情况下,buffer① 是一个指针变量,指向局部变量,然后此局部变量通过 buffer 传递到新线程中②。此时,函数 oops 很有可能会在 buffer 转换成std::string对象之前结束,从而导致一些未定义的行为。因为此时无法保证隐式转换的操作和 std::thread 构造函数的拷贝操作按顺序进行,有可能 std::thread 的构造函数拷贝的是转换前的变量(buffer 指针),而非字符串。

解决方案:在传递到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] = { 0 };
  5. std::cin >> buffer;
  6. std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针
  7. t.detach();
  8. }

2.2 结构体的传递

比如,你可能会尝试使用线程更新一个引用传递的结构体:

  1. struct Data
  2. {
  3. int height;
  4. int weight;
  5. };
  6. void struct_f(int i, Data& data)
  7. {
  8. cout << "struct_f, input parameters: "<< i << "; height:" << data.height << ", weight:" << data.weight << endl;
  9. }
  10. void main()
  11. {
  12. Data data = { 20,25 };
  13. thread t3(struct_f, 3, data);//直接传递结构体,线程无法完成值拷贝,编译不通过
  14. t3.join();
  15. }

std::thread 的构造函数无视函数期待的参数类型,并盲目地拷贝已提供的变量(右值拷贝)。因为函数期望的是一个非常量引用作为参数,而非右值,所以会在编译时出错。

解决方案:可以使用**std::ref**将参数转换成引用的形式,因此可将线程的调用改为以下形式:

  1. thread t3(struct_f, 3, std::ref(data));//使用ref,直接传递引用到线程,而不是复制一份

从而,struct_f就会接收到一个 data 变量的引用,而非 data 变量的拷贝副本,这样代码就能顺利的通过编译。

2.3 传递类成员函数

比如,你也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数, std::thread 构造函数的第三个参数就是成员函数的第一个参数,以此类推:

  1. class X
  2. {
  3. public:
  4. void do_work(int i)
  5. {
  6. cout << "I am a function in class X, input parameter: " << i << endl;
  7. }
  8. };
  9. int main()
  10. {
  11. X my_x;
  12. std::thread t4(&X::do_work, &my_x, 4);//1, 传递类里函数,对象,和函数所需参数
  13. t4.join();
  14. }

这段代码中,新线程将会调用 my_x.do_work(),其中 my_x 的地址 ① 作为对象指针提供给函数,第三个参数4为传入的函数参数。

2.4 std::move传递指针

当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要使用std::move()进行显示移动。下面的代码展示了std::move的用法,展示了std::move是如何转移一个动态对象的所有权到一个线程中去的:

  1. void pointer_f(int i, std::unique_ptr<Data> data)
  2. {
  3. cout << "pointer_f, input parameters: " << i << "; height:" << data->height << ", weight:" << data->weight << endl;
  4. }
  5. int main()
  6. {
  7. std::unique_ptr<Data> data2(new Data);////唯一智能指针,不可复制
  8. data2->height = 50;
  9. data2->weight = 55;
  10. thread t5(pointer_f, 5, std::move(data2));//指针无法使用std::ref,使用move转移所有权到线程
  11. t5.join();
  12. }

通过在 std::thread 的构造函数中执行 std::move(data2),Data 对象的所有权首先被转移到新创建线程的的内部存储中,之后再被传递给 pointer_f 函数。

3. 转移线程所有权

3.1 std::move转移线程所有权

  1. void fun1();
  2. void fun2();
  3. std::thread t1(some_function); // 1
  4. std::thread t2=std::move(t1); // 2
  5. t1=std::thread(fun2); // 3
  6. std::thread t3; // 4
  7. t3=std::move(t2); // 5
  8. t1=std::move(t3); // 6 赋值操作将使程序崩溃
  1. 线程t1与fun1函数关联
  2. 使用move之后,线程所有权转移到了t2,即t2与fun1关联,t1线程没有关联的函数
  3. 给t1重新关联函数,之后t1与fun2函数关联(新创建的fun2 thread是一个临时对象,所以可以隐式的进行移动,不需要move
  4. 创建线程t3,没有关联函数
  5. 将所有权从t2,转移到t3,即t3与fun1关联(此时,t3关联fun1,t1关联fun2,t2空闲)
  6. 错误操作,t1已经有执行函数,不能在结束前赋值一个新的执行线程

3.2 thread作为函数参数和返回值

std::thread支持移动,就意味着线程的所有权可以在函数外进行转移(作为函数返回值return):

  1. std::thread f()
  2. {
  3. void some_function();
  4. return std::thread(some_function);//返回thread,这里会隐式的被移动,而不是复制
  5. }

thread支持移动,相似的,作为函数参数也会触发移动操作:

  1. void f(std::thread t);
  2. void g()
  3. {
  4. void some_function();
  5. f(std::thread(some_function));
  6. std::thread t(some_function);
  7. f(std::move(t));
  8. }

4. 运行时决定线程数量

std::thread::hardware_concurrency()在C++标准中是一个很有用的函数。这个函数会返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。

  1. #include <iostream>
  2. #include <thread>
  3. int main() {
  4. unsigned int n = std::thread::hardware_concurrency();
  5. std::cout << n << " concurrent threads are supported.\n";
  6. }

5. 线程标识

线程标识类型为std::thread::id,并可以通过两种方式进行获取:

  • 第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值,这个值表示“无线程”。
  • 第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在<thread>头文件中)也可以获得线程标识。
  1. int main()
  2. {
  3. thread th(hello);
  4. th.join();
  5. thread::id childId = th.get_id();//获取指定线程对象的id
  6. thread::id masterId = this_thread::get_id();//获取当前线程的id
  7. cout << "child thread id:" << childId << endl;
  8. cout << "master thread id:" << masterId << endl;
  9. cout << (masterId == childId) << endl;
  10. return 0;
  11. }

如果两个对象的std::thread::id相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。

std::thread::id类型对象提供相当丰富的对比操作;比如,提供为不同的值进行排序。程序员将其当做为容器的键值,做排序,或做其他方式的比较。按默认顺序比较不同值的std::thread::id,所以这个行为可预见的:当a<bb<c时,得a<c,等等。标准库也提供std::hash<std::thread::id>容器,所以std::thread::id也可以作为无序容器的键值

std::thread::id实例常用作检测线程是否需要进行一些操作,比如:主线程可能要做一些与其他线程不同的工作,需要对主线程进行区分,做法就是获取当前线程Id,是否和保存的主线程Id一致。

  1. std::thread::id master_thread;
  2. void some_core_part_of_algorithm()
  3. {
  4. if(std::this_thread::get_id()==master_thread)
  5. {
  6. do_master_thread_work();
  7. }
  8. do_common_work();
  9. }

6 中断线程(没懂,留作后续研究)

很多情况下,使用信号来终止一个长时间运行的线程是合理的。这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户显式的取消了这个任务,亦或其他各种原因。不管是什么原因,原理都一样:需要使用信号来让未结束线程停止运行。这需要一种合适的方式让线程主动的停下来,而非戛然而止。

可能会给每种情况制定一个独立的机制,但这样做的意义不大。不仅因为用统一的机制会更容易在之后的场景中实现,而且写出来的中断代码不用担心在哪里使用。C11标准没有提供这样的机制(草案上有积极的建议,说不定中断线程会在以后的C标准中添加[1]),不过实现这样的机制也并不困难。

了解一下应该如何实现这种机制前,先来了解一下启动和中断线程的接口。

6.1 启动和中断线程

先看一下外部接口,需要从可中断线程上获取些什么?最起码需要和std::thread相同的接口,还要多加一个interrupt()函数:

  1. class interruptible_thread
  2. {
  3. public:
  4. template<typename FunctionType>
  5. interruptible_thread(FunctionType f);
  6. void join();
  7. void detach();
  8. bool joinable() const;
  9. void interrupt();
  10. };

类内部可以使用std::thread来管理线程,并且使用一些自定义数据结构来处理中断。现在,从线程的角度能看到什么呢?“能用这个类来中断线程”——需要一个断点(interruption point)。在不添加多余的数据的前提下,为了使断点能够正常使用,就需要使用一个没有参数的函数:interruption_point()。这意味着中断数据结构可以访问thread_local变量,并在线程运行时,对变量进行设置,因此当线程调用interruption_point()函数时,就会去检查当前运行线程的数据结构。我们将在后面看到interruption_point()的具体实现。

thread_local标志是不能使用普通的std::thread管理线程的主要原因;需要使用一种方法分配出一个可访问的interruptible_thread实例,就像新启动一个线程一样。使用已提供函数来做这件事情前,需要将interruptible_thread实例传递给std::thread的构造函数,创建一个能够执行的线程,就像下面的代码清单所实现。

清单 interruptible_thread的基本实现

  1. class interrupt_flag
  2. {
  3. public:
  4. void set();
  5. bool is_set() const;
  6. };
  7. thread_local interrupt_flag this_thread_interrupt_flag; // 1
  8. class interruptible_thread
  9. {
  10. std::thread internal_thread;
  11. interrupt_flag* flag;
  12. public:
  13. template<typename FunctionType>
  14. interruptible_thread(FunctionType f)
  15. {
  16. std::promise<interrupt_flag*> p; // 2
  17. internal_thread=std::thread([f,&p]{ // 3
  18. p.set_value(&this_thread_interrupt_flag);
  19. f(); // 4
  20. });
  21. flag=p.get_future().get(); // 5
  22. }
  23. void interrupt()
  24. {
  25. if(flag)
  26. {
  27. flag->set(); // 6
  28. }
  29. }
  30. };

提供函数f是包装了一个Lambda函数③,线程将会持有f副本和本地承诺值变量(p)的引用②。新线程中,Lambda函数设置承诺值变量的值到this_thread_interrupt_flag(在thread_local①中声明)的地址中,为的是让线程能够调用提供函数的副本④。调用线程会等待与其期望值相关的承诺值就绪,并且将结果存入到flag成员变量中⑤。注意,即使Lambda函数在新线程上执行,对本地变量p进行悬空引用都没有问题,因为在新线程返回之前,interruptible_thread构造函数会等待变量p,直到变量p不被引用。实现没有考虑汇入线程或分离线程,所以需要flag变量在线程退出或分离前已经声明,这样就能避免悬空问题。

interrupt()函数相对简单:需要线程去做中断时,需要合法指针作为中断标志,所以可以对标志进行设置⑥。

6.2 检查线程是否中断

现在就可以设置中断标志了,不过不检查线程是否被中断,意义就不大了。使用interruption_point()函数最简单的情况;可以在安全的地方调用这个函数,如果标志已经设置,就可以抛出一个thread_interrupted异常:

  1. void interruption_point()
  2. {
  3. if(this_thread_interrupt_flag.is_set())
  4. {
  5. throw thread_interrupted();
  6. }
  7. }

代码中可以在适当的地方使用这个函数:

  1. void foo()
  2. {
  3. while(!done)
  4. {
  5. interruption_point();
  6. process_next_item();
  7. }
  8. }

虽然也能工作,但不理想。最好是在线程等待或阻塞的时候中断线程,因为这时的线程不能运行,也就不能调用interruption_point()函数!线程等待时,什么方式才能去中断线程呢?

6.3 std::condition_variable_any中断等待

std::condition_variable_anystd::condition_variable的不同在于,std::condition_variable_any可以使用任意类型的锁,而不仅有std::unique_lock<std::mutex>。可以让事情做起来更加简单,并且std::condition_variable_any可以比std::condition_variable做的更好。因为能与任意类型的锁一起工作,就可以设计自己的锁,上锁/解锁interrupt_flag的内部互斥量set_clear_mutex,并且锁也支持等待调用,就像下面的代码。

std::condition_variable_any设计的interruptible_wait

  1. class interrupt_flag
  2. {
  3. std::atomic<bool> flag;
  4. std::condition_variable* thread_cond;
  5. std::condition_variable_any* thread_cond_any;
  6. std::mutex set_clear_mutex;
  7. public:
  8. interrupt_flag():
  9. thread_cond(0),thread_cond_any(0)
  10. {}
  11. void set()
  12. {
  13. flag.store(true,std::memory_order_relaxed);
  14. std::lock_guard<std::mutex> lk(set_clear_mutex);
  15. if(thread_cond)
  16. {
  17. thread_cond->notify_all();
  18. }
  19. else if(thread_cond_any)
  20. {
  21. thread_cond_any->notify_all();
  22. }
  23. }
  24. template<typename Lockable>
  25. void wait(std::condition_variable_any& cv,Lockable& lk)
  26. {
  27. struct custom_lock
  28. {
  29. interrupt_flag* self;
  30. Lockable& lk;
  31. custom_lock(interrupt_flag* self_,
  32. std::condition_variable_any& cond,
  33. Lockable& lk_):
  34. self(self_),lk(lk_)
  35. {
  36. self->set_clear_mutex.lock(); // 1
  37. self->thread_cond_any=&cond; // 2
  38. }
  39. void unlock() // 3
  40. {
  41. lk.unlock();
  42. self->set_clear_mutex.unlock();
  43. }
  44. void lock()
  45. {
  46. std::lock(self->set_clear_mutex,lk); // 4
  47. }
  48. ~custom_lock()
  49. {
  50. self->thread_cond_any=0; // 5
  51. self->set_clear_mutex.unlock();
  52. }
  53. };
  54. custom_lock cl(this,cv,lk);
  55. interruption_point();
  56. cv.wait(cl);
  57. interruption_point();
  58. }
  59. // rest as before
  60. };
  61. template<typename Lockable>
  62. void interruptible_wait(std::condition_variable_any& cv,
  63. Lockable& lk)
  64. {
  65. this_thread_interrupt_flag.wait(cv,lk);
  66. }

自定义的锁类型在构造的时候,需要所锁住内部set_clear_mutex①,对thread_cond_any指针进行设置,并引用std::condition_variable_any传入锁的构造函数中②。可锁的引用将会在之后进行存储,其变量必须被锁住。现在可以安心的检查中断,不用担心竞争了。如果中断标志已经设置,那么标志是在锁住set_clear_mutex时设置的。当条件变量调用自定义锁的unlock()函数中的wait()时,就会对可锁对象和set_clear_mutex进行解锁③。这就允许线程可以尝试中断其他线程获取set_clear_mutex锁;以及在内部wait()调用之后,检查thread_cond_any指针。这就是在替换std::condition_variable后,所拥有的功能(不包括管理)。当wait()结束等待(因为等待,或因为伪苏醒),因为线程将会调用lock()函数,依旧要求锁住内部set_clear_mutex,并且锁住可锁对象④。wait()调用时,custom_lock的析构函数中⑤清理thread_cond_any指针(同样会解锁set_clear_mutex)之前,可以再次对中断进行检查。

6.4 中断其他阻塞调用

这次轮到中断条件变量的等待了,不过其他阻塞情况,比如:互斥锁,等待期望值等等,该怎么处理呢?通常情况下,可以使用std::condition_variable的超时选项,因为实际运行中不可能很快的将条件变量的等待终止(不访问内部互斥量或期望值的话)。不过,某些情况下知道在等待什么,就可以让循环在interruptible_wait()函数中运行。作为一个例子,这里为std::future<>重载了interruptible_wait()的实现:

  1. template<typename T>
  2. void interruptible_wait(std::future<T>& uf)
  3. {
  4. while(!this_thread_interrupt_flag.is_set())
  5. {
  6. if(uf.wait_for(lk,std::chrono::milliseconds(1))==
  7. std::future_status::ready)
  8. break;
  9. }
  10. interruption_point();
  11. }

等待会在中断标志设置好的时候,或future准备就绪的时候停止,不过实现中每次等待期望值的时间只有1ms。这就意味着,中断请求被确定前,平均等待的时间为0.5ms(这里假设存在一个高精度的时钟)。通常wait_for至少会等待一个时钟周期,如果时钟周期为15ms,那么结束等待的时间将会是15ms,而不是1ms。接受与不接受这种情况,都得视情况而定。如果必要且时钟支持的话,可以持续削减超时时间。这种方式将会让线程苏醒很多次来检查标志,并且增加线程切换的开销。

OK,我们已经了解如何使用interruption_point()和interruptible_wait()函数检查中断。当中断被检查出来了,要如何处理它呢?

6.5 处理中断

从中断线程的角度看,中断就是thread_interrupted异常,因此能像处理其他异常那样进行处理。特别是使用标准catch块对其进行捕获:

  1. try
  2. {
  3. do_something();
  4. }
  5. catch(thread_interrupted&)
  6. {
  7. handle_interruption();
  8. }

捕获中断,进行处理。其他线程再次调用interrupt()时,线程将会再次被中断,这就被称为断点(interruption point)。如果线程执行的是一系列独立的任务,就会需要断点;中断一个任务,就意味着这个任务被丢弃,并且该线程就会执行任务列表中的其他任务。

因为thread_interrupted是一个异常,在能够被中断的代码中,之前线程安全的注意事项都是适用的,就是为了确保资源不会泄露,并在数据结构中留下对应的退出状态。通常,线程中断是可行的,所以只需要让异常传播即可。不过,当异常传入std::thread的析构函数时,将会调用std::terminate(),并且整个程序将会终止。为了避免这种情况,需要在每个将interruptible_thread变量作为参数传入的函数中放置catch(thread_interrupted)处理块,可以将catch块包装进interrupt_flag的初始化过程中。因为异常将会终止独立进程,这样就能保证未处理的中断是异常安全的。interruptible_thread构造函数中对线程的初始化,实现如下:

  1. internal_thread=std::thread([f,&p]{
  2. p.set_value(&this_thread_interrupt_flag);
  3. try
  4. {
  5. f();
  6. }
  7. catch(thread_interrupted const&)
  8. {}
  9. });

下面,我们来看个更加复杂的例子。

6.6 退出时中断后台任务

试想在桌面上查找一个应用。这就需要与用户互动,应用的状态需要能在显示器上显示,就能看出应用有什么改变。为了避免影响GUI的响应时间,通常会将处理线程放在后台运行。后台进程需要一直执行,直到应用退出;后台线程会作为应用启动的一部分被启动,并且在应用终止的时候停止运行。通常这样的应用只有在机器关闭时,才会退出,因为应用需要更新应用最新的状态,就需要全时间运行。在某些情况下,当应用被关闭,需要使用有序的方式将后台线程关闭,其中一种方式就是中断。

下面清单中为一个系统实现了简单的线程管理部分。后台监视文件系统:

  1. std::mutex config_mutex;
  2. std::vector<interruptible_thread> background_threads;
  3. void background_thread(int disk_id)
  4. {
  5. while(true)
  6. {
  7. interruption_point(); // 1
  8. fs_change fsc=get_fs_changes(disk_id); // 2
  9. if(fsc.has_changes())
  10. {
  11. update_index(fsc); // 3
  12. }
  13. }
  14. }
  15. void start_background_processing()
  16. {
  17. background_threads.push_back(
  18. interruptible_thread(background_thread,disk_1));
  19. background_threads.push_back(
  20. interruptible_thread(background_thread,disk_2));
  21. }
  22. int main()
  23. {
  24. start_background_processing(); // 4
  25. process_gui_until_exit(); // 5
  26. std::unique_lock<std::mutex> lk(config_mutex);
  27. for(unsigned i=0;i<background_threads.size();++i)
  28. {
  29. background_threads[i].interrupt(); // 6
  30. }
  31. for(unsigned i=0;i<background_threads.size();++i)
  32. {
  33. background_threads[i].join(); // 7
  34. }
  35. }

启动时,后台线程就已经启动④。之后,对应线程将会处理GUI⑤。用户要求进程退出时,后台进程将会被中断⑥,并且主线程会等待每一个后台线程结束后才退出⑦。后台线程运行在一个循环中,并时刻检查磁盘的变化②,对其序号进行更新③。调用interruption_point()函数①,可以在循环中对中断进行检查。

为什么中断线程前,会对线程进行等待?为什么不中断每个线程,让它们执行下一个任务?答案就是“并发”。线程被中断后,不会马上结束,因为需要对下一个断点进行处理,并且在退出前执行析构函数和代码异常处理部分。因为需要汇聚每个线程,所以就会让中断线程等待,即使线程还在做着有用的工作——中断其他线程。只有当没有工作时(所有线程都被中断)不需要等待。这就允许中断线程并行的处理自己的中断,并更快的完成中断。