假设要写一个在后台启动线程的函数,并想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。所以,新线程的所有权都需要转移。

    这就是将移动操作引入std::thread的原因,C++标准库中有很多资源占有(resource-owning)类型,比如std::ifstreamstd::unique_ptr还有std::thread都是可移动,但不可拷贝。这就说明执行线程的所有权可以在std::thread实例中移动,下面将展示一个例子。例子中,创建了两个执行线程,并且在std::thread实例之间(t1,t2和t3)转移所有权:

    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 赋值操作将使程序崩溃

    首先,新线程开始与t1相关联①。当显式使用std::move()创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行some_function的函数线程与t2关联。

    然后,一个临时std::thread对象相关的线程启动了③。为什么不显式调用std::move()转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用。

    t3使用默认构造方式创建④,与任何执行线程都没有关联。调用std::move()将与t2关联线程的所有权转移到t3中⑤。因为t2是一个命名对象,需要显式的调用std::move()。移动操作 #5 完成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行some_function的线程相关联。

    最后一个移动操作,将some_function线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序继续运行。这样做(不抛出异常,std::terminate()是noexcept函数)是为了保证与std::thread的析构函数的行为一致。2.1.1节中,需要在线程对象被析构前,显式的等待线程完成,或者分离它;进行赋值时也需要满足这些条件(说明:不能通过赋一个新值给std::thread对象的方式来“丢弃”一个线程)。

    std::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. }

    std::thread支持移动的好处是可以创建thread_guard类的实例(定义见清单2.3),并且拥有其线程所有权。当thread_guard对象所持有的线程被引用时,移动操作就可以避免很多不必要的麻烦;这意味着,当某个对象转移了线程的所有权后,它就不能对线程进行加入或分离。为了确保线程程序退出前完成,下面的代码里定义了scoped_thread类。现在,我们来看一下这段代码:

    清单2.6 scoped_thread的用法

    1. class scoped_thread {
    2. std::thread t;
    3. public:
    4. explicit scoped_thread(std::thread t_) // 1
    5. : t(std::move(t_))
    6. {
    7. if (!t.joinable()) // 2
    8. throw std::logic_error(“No thread”);
    9. }
    10. ~scoped_thread()
    11. {
    12. t.join(); // 3
    13. }
    14. scoped_thread(scoped_thread const&) = delete;
    15. scoped_thread& operator=(scoped_thread const&) = delete;
    16. };
    17. struct func; // 定义在清单2.1中
    18. void f()
    19. {
    20. int some_local_state;
    21. scoped_thread t(std::thread(func(some_local_state))); // 4
    22. do_something_in_current_thread();
    23. } // 5

    与清单2.3相似,不过新线程直接传递到scoped_thread中④,而非创建一个独立变量。当主线程到达f()函数末尾时⑤,scoped_thread对象就会销毁,然后加入③到的构造函数①创建的线程对象中去。在清单2.3中的thread_guard类,需要在析构中检查线程是否”可加入”。这里把检查放在了构造函数中②,并且当线程不可加入时,抛出异常。

    这里对C++17标准给出一个建议,就是添加一个joining_thread的类型,这个类型与std::thread类似;不同是的添加了析构函数,就类似于scoped_thread。委员会成员们对此并没有达成统一共识,所以这个类没有添加入C++17标准中(C++20仍旧对这种方式进行探讨,不过名称为std::jthread),不过这个类实现起来也不是很困难。下面就来对这个类进行实现:

    清单2.7 joining_thread类的实现

    1. class joining_thread {
    2. std::thread t;
    3. public:
    4. joining_thread() noexcept = default;
    5. template <typename Callable, typename... Args>
    6. explicit joining_thread(Callable&& func, Args&&... args)
    7. : t(std::forward<Callable>(func), std::forward<Args>(args)...)
    8. {
    9. }
    10. explicit joining_thread(std::thread t_) noexcept
    11. : t(std::move(t_))
    12. {
    13. }
    14. joining_thread(joining_thread&& other) noexcept
    15. : t(std::move(other.t))
    16. {
    17. }
    18. joining_thread& operator=(joining_thread&& other) noexcept
    19. {
    20. ifjoinable()
    21. {
    22. join();
    23. }
    24. t = std::move(other.t);
    25. return *this;
    26. }
    27. joining_thread& operator=(std::thread other) noexcept
    28. {
    29. if (joinable())
    30. join();
    31. t = std::move(other);
    32. return *this;
    33. }
    34. ~joining_thread() noexcept
    35. {
    36. if (joinable())
    37. join();
    38. }
    39. void swap(joining_thread& other) noexcept
    40. {
    41. t.swap(other.t);
    42. }
    43. std::thread::id get_id() const noexcept
    44. {
    45. return t.get_id();
    46. }
    47. bool joinable() const noexcept
    48. {
    49. return t.joinable();
    50. }
    51. void join()
    52. {
    53. t.join();
    54. }
    55. void detach()
    56. {
    57. t.detach();
    58. }
    59. std::thread& as_thread() noexcept
    60. {
    61. return t;
    62. }
    63. const std::thread& as_thread() const noexcept
    64. {
    65. return t;
    66. }
    67. };

    std::thread对象的容器,如果这个容器是移动敏感的(比如,标准中的std::vector<>),那么移动操作同样适用于这些容器。了解这些后,就可以写出类似清单2.7中的代码,代码量产了一些线程,并且等待它们结束。

    清单2.8 量产线程,等待它们结束

    1. void do_work(unsigned id);
    2. void f()
    3. {
    4. std::vector<std::thread> threads;
    5. for (unsigned i = 0; i < 20; ++i) {
    6. threads.push_back(std::thread(do_work, i)); // 产生线程
    7. }
    8. std::for_each(threads.begin(), threads.end(),
    9. std::mem_fn(&std::thread::join)); // 对每个线程调用join()
    10. }

    我们经常需要线程去分割一个算法的工作总量,所以在算法结束的之前,所有的线程必须结束。清单2.8中线程所做的工作都是独立的,并且结果仅会受到共享数据的影响。如果f()有返回值,这个返回值就依赖于线程得到的结果。在写入返回值之前,程序会检查使用共享数据的线程是否终止。结果在不同线程中转移的替代方案,我们会在第4章中再次讨论。

    std::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,并且直接加入,而是把它们当做一个组。创建一组线程(数量在运行时确定),可使得这一步迈的更大,而非像清单2.8那样创建固定数量的线程。