处理突变

如果不同时读写数据,就没有数据竞争,最简单的方法是使用不可变值。除此之外,还有两种典型的策略。首先,用锁来保护临界区,例如:范围锁或策略锁。在面向对象设计中,关键部分的通常是对象(包括它的接口),线程安全的接口会保护整个对象。其次,修改线程只是在工作完成时发出信号,这就是保护性暂挂模式。

范围锁

范围锁是将RAII(资源获取即初始化)应用于互斥锁,这个用法的关键思想是将资源获取和释放绑定到对象的生存期。顾名思义,对象的生命周期范围是确定的。这里的范围意味着,C++运行时会负责调用对象的析构函数,从而释放资源。

ScopedLock类实现了范围锁。

  1. // scopedLock.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. #include <new>
  5. #include <string>
  6. #include <utility>
  7. class ScopedLock{
  8. private:
  9. std::mutex& mut;
  10. public:
  11. explicit ScopedLock(std::mutex& m) :mut(m) {
  12. mut.lock();
  13. std::cout << "Lock the mutex: " << &mut << std::endl;
  14. }
  15. ~ScopedLock() {
  16. std::cout << "Release the mutex: " << &mut << std::endl;
  17. mut.unlock();
  18. }
  19. };
  20. int main() {
  21. std::cout << std::endl;
  22. std::mutex mutex1;
  23. ScopedLock scopedLock1{ mutex1 };
  24. std::cout << "\nBefore local scope" << std::endl;
  25. {
  26. std::mutex mutex2;
  27. ScopedLock scopedLock2{ mutex2 };
  28. }
  29. std::cout << "After local scope" << std::endl;
  30. std::cout << "\nBefore try-catch block" << std::endl;
  31. try {
  32. std::mutex mutex3;
  33. ScopedLock scopedLoack3{ mutex3 };
  34. throw std::bad_alloc();
  35. }
  36. catch (std::bad_alloc& e) {
  37. std::cout << e.what();
  38. }
  39. std::cout << "\nAfter try-catch block" << std::endl;
  40. std::cout << std::endl;
  41. }

ScopedLock通过引用的方式获取互斥对象(第13行)。互斥量在构造函数(第15行)中锁定,在析构函数(第19行)中进行解锁。由于RAII的使用习惯,对象在销毁时,会自动对互斥量进行解锁。

处理突变 - 图1

scopedLock1的作用域在主函数体中。因此,mutex1最后才解锁,mutex2(第34行)和mutex3(第42行)也是同理。对于mutex3而言,如果触发异常,也会调用scopedLock3的析构函数。有趣的是,mutex3重用了mutex2的内存,因为两者的地址相同。

C++17支持四种类型的锁:std::lock_guard/std::scoped_lock用于简单场景,std::unique_lock/std::shared_lock用于高级场景,可以在关于锁的章节中了解更多的细节。

拓展阅读

策略锁

编写代码库时,这个库可用于各种领域,包括并发。为了安全起见,要用锁来保护关键部分。倘若库在单线程环境中运行,因为实现使用了重量级同步机制,则会存在性能问题。那么,现在就轮到策略锁登场了。

策略锁是将策略模式的思想应用于锁。这意味着,会将锁定策略放到实例对象中,并使其成为一个可热插拔的组件。那么,什么是策略模式呢?

Strategy Pattern

处理突变 - 图2

策略模式是《设计模式:可重用的面向对象软件元素》一书中经典的行为模式之一。其关键思想是定义一系列算法,将它们封装在一个对象中,从而使其成为可热插拔的组件。

策略模式

  1. // strategy.cpp
  2. #include <iostream>
  3. #include <memory>
  4. class Strategy {
  5. public:
  6. virtual void operator()() = 0;
  7. virtual ~Strategy() = default;
  8. };
  9. class Context {
  10. std::shared_ptr<Strategy> _start;
  11. public:
  12. explicit Context() : _start(nullptr) {}
  13. void setStrategy(std::shared_ptr<Strategy> start) { _start = start; }
  14. void strategy() { if (_start)(*_start)(); }
  15. };
  16. class Strategy1 :public Strategy {
  17. void operator()() override {
  18. std::cout << "Foo" << std::endl;
  19. }
  20. };
  21. class Strategy2 : public Strategy {
  22. void operator()() override {
  23. std::cout << "Bar" << std::endl;
  24. }
  25. };
  26. class Strategy3 :public Strategy {
  27. void operator()() override {
  28. std::cout << "FooBar" << std::endl;
  29. }
  30. };
  31. int main() {
  32. std::cout << std::endl;
  33. Context con;
  34. con.setStrategy(std::shared_ptr<Strategy>(new Strategy1));
  35. con.strategy();
  36. con.setStrategy(std::shared_ptr<Strategy>(new Strategy2));
  37. con.strategy();
  38. con.setStrategy(std::shared_ptr<Strategy>(new Strategy3));
  39. con.strategy();
  40. std::cout << std::endl;
  41. }

第6至10行中的抽象类Strategy定义了策略。每个特定的策略,如Strategy1(第20行)、Strategy2(第26行)或Strategy3(第32行),都必须支持函数调用操作符(第8行)。使用者在Context中集合了各种策略,在第16行设置特定的策略,并在第17行执行它。因为Context通过一个指向Strategy类的指针来执行,所以Strategy1Strategy2Strategy3的执行方法是私有的。

处理突变 - 图3

具体实现

实现策略锁有两种经典的方法:运行时多态性(面向对象)和编译时多态性(模板)。两种方式各有利弊。

  • 优点:
    • 运行时多态
      • 允许在运行时配置策略锁。
      • 了解有面向对象的开发人员,更容易理解。
    • 编译时多态
      • 无抽象的惩罚。
      • 扁平的层次结构。
  • 缺点:
    • 运行时多态
      • 额外需要一个指针。
      • 可能有很深的派生层次。
    • 编译时多态
      • 出错时会有非常详细的信息。

讨论了理论之后,我在两个程序中实现了策略锁。示例中,策略锁可支持无锁、独占锁和共享锁。简单起见,我在内部使用了互斥锁。此外,策略锁的模型也限定了其锁定的范围。

运行时多态

strategizedLockingRuntime.cpp程序中展示了三种互斥锁。

运行时的多态性策略锁

  1. // strategizedLockingRuntime.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. #include <shared_mutex>
  5. class Lock {
  6. public:
  7. virtual void lock() const = 0;
  8. virtual void unlock() const = 0;
  9. };
  10. class StrategizedLocking {
  11. Lock& lock;
  12. public:
  13. StrategizedLocking(Lock& l) :lock(l) {
  14. lock.lock();
  15. }
  16. ~StrategizedLocking() {
  17. lock.unlock();
  18. }
  19. };
  20. struct NullObjectMutex {
  21. void lock() {};
  22. void unlock() {};
  23. };
  24. class NoLock :public Lock {
  25. void lock() const override {
  26. std::cout << "NoLock::lock: " << std::endl;
  27. nullObjectMutex.lock();
  28. }
  29. void unlock() const override {
  30. std::cout << "NoLock::unlock: " << std::endl;
  31. nullObjectMutex.unlock();
  32. }
  33. mutable NullObjectMutex nullObjectMutex;
  34. };
  35. class ExclusiveLock : public Lock {
  36. void lock() const override {
  37. std::cout << " ExclusiveLock::lock: " << std::endl;
  38. mutex.lock();
  39. }
  40. void unlock() const override {
  41. std::cout << " ExclusiveLock::unlock: " << std::endl;
  42. mutex.unlock();
  43. }
  44. mutable std::mutex mutex;
  45. };
  46. class SharedLock : public Lock {
  47. void lock() const override {
  48. std::cout << " SharedLock::lock_shared: " << std::endl;
  49. sharedMutex.lock_shared();
  50. }
  51. void unlock() const override {
  52. std::cout << " SharedLock::unlock_shared: " << std::endl;
  53. sharedMutex.unlock_shared();
  54. }
  55. mutable std::shared_mutex sharedMutex;
  56. };
  57. int main() {
  58. std::cout << std::endl;
  59. NoLock noLock;
  60. StrategizedLocking stratLock1{ noLock };
  61. {
  62. ExclusiveLock exLock;
  63. StrategizedLocking stratLock2{ exLock };
  64. {
  65. SharedLock sharLock;
  66. StrategizedLocking startLock3{ sharLock };
  67. }
  68. }
  69. std::cout << std::endl;
  70. }

StrategizedLocking类中有一把锁(第14行)。StrategizedLocking模型是范围锁,因此在构造函数(第16行)中进行锁定,在析构函数(第19行)中进行解锁。Lock(第7 - 11行)是一个抽象类,定义了所有接口。派生类分别是NoLock (第29行)、ExclusiveLock(第41行)和SharedLock(第53行)。SharedLockstd::shared_mutex上可使用lock_shared(第56行)和unlock_shared进行锁定和解锁。每个锁持有一个互斥对象NullObjectMutex(第38行)、std::mutex(第50行)或std::shared_mutex(第62行)。其实,NullObjectMutex就是一个无操作的占位符。互斥对象声明为可变,就意味着可以用在常量方法中使用,比如:lock和unlock中。

空对象

类NullObjectMutex是空对象模式的一个例子,由空方法组成,算是一个占位符,这样便于优化器可以将它完全删除。

编译时多态

基于模板的实现与基于面向对象的实现非常相似。

编译时多态性策略锁

  1. // StrategizedLockingCompileTime.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. #include <shared_mutex>
  5. template <typename LOCK>
  6. class StrategizedLocking {
  7. LOCK& lock;
  8. public:
  9. StrategizedLocking(LOCK& l) :lock(l) {
  10. lock.lock();
  11. }
  12. ~StrategizedLocking() {
  13. lock.unlock();
  14. }
  15. };
  16. struct NullObjectMutex {
  17. void lock() {};
  18. void unlock() {};
  19. };
  20. class NoLock {
  21. public:
  22. void lock() const {
  23. std::cout << "NoLock::lock: " << std::endl;
  24. nullObjectMutex.lock();
  25. }
  26. void unlock() const {
  27. std::cout << "NoLock::unlock: " << std::endl;
  28. nullObjectMutex.unlock();
  29. }
  30. mutable NullObjectMutex nullObjectMutex;
  31. };
  32. class ExclusiveLock {
  33. public:
  34. void lock() const {
  35. std::cout << " ExclusiveLock::lock: " << std::endl;
  36. mutex.lock();
  37. }
  38. void unlock() const {
  39. std::cout << " ExclusiveLock::unlock: " << std::endl;
  40. mutex.unlock();
  41. }
  42. mutable std::mutex mutex;
  43. };
  44. class SharedLock {
  45. public:
  46. void lock() const {
  47. std::cout << " SharedLock::lock_shared: " << std::endl;
  48. sharedMutex.lock_shared();
  49. }
  50. void unlock() const {
  51. std::cout << " SharedLock::unlock_shared: " << std::endl;
  52. sharedMutex.unlock_shared();
  53. }
  54. mutable std::shared_mutex sharedMutex;
  55. };
  56. int main() {
  57. std::cout << std::endl;
  58. NoLock noLock;
  59. StrategizedLocking stratLock1{ noLock };
  60. {
  61. ExclusiveLock exLock;
  62. StrategizedLocking stratLock2{ exLock };
  63. {
  64. SharedLock sharLock;
  65. StrategizedLocking startLock3{ sharLock };
  66. }
  67. }
  68. std::cout << std::endl;
  69. }

这次NoLock(第25行)、ExclusiveLock(第38行)和SharedLock(第51行)没有抽象的基类了。结果StrategizedLocking可以用不支持相应接口的对象进行实例化,而这将导致编译时错误。C++20中,可以使用Lockable : template <Lockable Lock> class StrategizedLocking代替template <typename Lock> class StrategizedLocking。这意味着所有使用的锁必须支持Lockable概念。概念需要命名,并且Lockable已经在C++20中定义了。如果没有满足此要求,则编译将失败,并出现简单易懂的错误消息。

两个程序会生成相同的输出:

处理突变 - 图4

拓展阅读

线程安全的接口

当程序的关键部分只是一个对象时,那么使用线程安全的接口就在合适不过了。用锁可能会导致性能问题,甚至会导致死锁。下面的伪代码可以清楚地阐明我的观点。

  1. struct Critical{
  2. void method1(){
  3. lock(mut);
  4. method2();
  5. ...
  6. }
  7. void method2(){
  8. lock(mut);
  9. ...
  10. }
  11. mutex mut;
  12. };
  13. Critical crit;
  14. crit.method1();

使用crit.method1会使互斥锁锁定两次。为了简单起见,这个锁是一个范围锁。当然,这里还有两个问题:

  1. lock是递归锁时,method2中的第二个lock(mut)是多余的。
  2. lock不是递归锁时,method2中的第二个lock(mut)会导致未定义行为。大多数情况下,会出现死锁。

线程安全的接口可以避免这两个问题,因为:

  • 所有(public)接口都应该使用锁。
  • 所有(保护的和私有的)方法都不使用锁。
  • 接口只能使用保护的方法或私有方法调用,而公共方法则不能调用。

threadSafeInterface.cpp程序显示了其用法。

  1. // threadSafeInterface.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. #include <shared_mutex>
  5. class Critical {
  6. public:
  7. void interface1() const {
  8. std::lock_guard<std::mutex> lockGuard(mut);
  9. implementation1();
  10. }
  11. void interface2() {
  12. std::lock_guard<std::mutex> lockGuard(mut);
  13. implementation2();
  14. implementation3();
  15. implementation1();
  16. }
  17. private:
  18. void implementation1() const {
  19. std::cout << "implementation1: "
  20. << std::this_thread::get_id() << std::endl;
  21. }
  22. void implementation2() const {
  23. std::cout << " implementation2: "
  24. << std::this_thread::get_id() << std::endl;
  25. }
  26. void implementation3() const {
  27. std::cout << " implementation3: "
  28. << std::this_thread::get_id() << std::endl;
  29. }
  30. mutable std::mutex mut;
  31. };
  32. int main() {
  33. std::cout << std::endl;
  34. std::thread t1([] {
  35. const Critical crit;
  36. crit.interface1();
  37. });
  38. std::thread t2([] {
  39. Critical crit;
  40. crit.interface2();
  41. crit.interface1();
  42. });
  43. Critical crit;
  44. crit.interface1();
  45. crit.interface2();
  46. t1.join();
  47. t2.join();
  48. std::cout << std::endl;
  49. }

包括主线程在内的三个线程都使用了Critical实例。由于线程安全的接口,所有对公开API的调用都是同步的。第35行中的互斥对象是可变的,因此可以在const方法implementation1中使用。

线程安全的接口有三个好处:

  1. 互斥锁不可能递归调用。在C++中,对非递归互斥对象的递归调用会导致未定义行为,通常都会死锁。
  2. 该程序使用最小范围的锁定,因此同步的代价最小。仅在关键类的公共或私有方法中使用std::recursive_mutex将产生重量级的同步,从而遭受性能惩罚。
  3. 从用户的角度来看,Critical很容易使用,而同步只是实现的一个细节而已。

三个线程交错的输出:

处理突变 - 图5

尽管线程安全的接口看起来很容易实现,但是也需要留意两个风险点。

风险

类中使用静态成员和使用虚接口时,需要特别小心。

静态成员

当类有静态成员时,就必须同步该类实例上的所有成员函数。

  1. class Critical {
  2. public:
  3. void interface1() const {
  4. std::lock_guard<std::mutex> lockGuard(mut);
  5. implementation1();
  6. }
  7. void interface2() {
  8. std::lock_guard<std::mutex> lockGuard(mut);
  9. implementation2();
  10. implementation3();
  11. implementation1();
  12. }
  13. private:
  14. void implementation1() const {
  15. std::cout << "implementation1: "
  16. << std::this_thread::get_id() << std::endl;
  17. ++called;
  18. }
  19. void implementation2() const {
  20. std::cout << " implementation2: "
  21. << std::this_thread::get_id() << std::endl;
  22. ++called;
  23. }
  24. void implementation3() const {
  25. std::cout << " implementation3: "
  26. << std::this_thread::get_id() << std::endl;
  27. ++called;
  28. }
  29. inline static int called{ 0 };
  30. inline static std::mutex mut;
  31. };

Critical类使用了静态成员(第32行)来计算调用成员函数的频率。Critical的所有实例,都使用同一个静态成员,因此必须同步。本例中,临界区为Critical的所有实例。

内联静态成员

C++17中,静态数据成员可以声明为内联。可以在类中定义,以及初始化内联静态数据成员。

  1. struct X
  2. {
  3. inline static int n = 1;
  4. }

虚接口

当重写虚接口函数时,即使重写的函数是私有的,也应该有锁。

  1. // threadSafeInterfaceVirtual.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. #include <thread>
  5. class Base {
  6. public:
  7. virtual void interface() {
  8. std::lock_guard<std::mutex> lockGuard(mut);
  9. std::cout << "Base with lock" << std::endl;
  10. }
  11. private:
  12. std::mutex mut;
  13. };
  14. class Derived : public Base {
  15. void interface() override {
  16. std::cout << "Derived without lock" << std::endl;
  17. };
  18. };
  19. int main() {
  20. std::cout << std::endl;
  21. Base* base1 = new Derived;
  22. base1->interface();
  23. Derived der;
  24. Base& base2 = der;
  25. base2.interface();
  26. std::cout << std::endl;
  27. }

base1->interfacebase2.interface中,base1base2是静态类型是Base,因此interface是一个公开接口。由于接口方法是虚函数,因此在运行时使用派生的动态类型Derived进行。最后,调用派生类Derived的私有接口。

处理突变 - 图6

有两种方法可以避免风险:

  1. 使接口成为非虚接口,这种技术称为NVI(非虚拟接口)
  2. 将接口声明为final: virtual void interface() final;

扩展阅读

保护性暂挂模式

锁和一些先决条件的组合,是构成保护性暂挂模式的基础件。如果未满足先决条件,则线程将自己置为休眠状态。为了避免数据竞争或死锁,检查线程时会使用锁。

现在,来看看各种情况:

  • 处于等待状态的线程,会根据通知更改状态,也可以主动请求更改状态。我把这称为“推拉原则”。
  • 等待可以有时限,也可以没有时限。
  • 可以将通知发送给一个或所有正在等待的线程。

推拉原则

先来说说推原则。

推原则

大多数情况下,使用条件变量或future/promise来进行线程同步。条件变量或promise将通知发送到正在等待的线程。promise没有notify_onenotify_all方法,而空的set_value调用通常用于模拟通知。下面的程序段展示发送通知的线程和等待的线程。

  • 条件变量
  1. void waitingForWork(){
  2. std::cout << "Worker: Waiting for work." << std::endl;
  3. std::unique_lock<std::mutex> lck(mutex_);
  4. condVar.wait(lck, []{ return dataReady; });
  5. doTheWork();
  6. std::cout << "Work done." << std::endl;
  7. }
  8. void setDataReady(){
  9. {
  10. std::lock_guard<std::mutex> lck(mutex_);
  11. dataReady = true;
  12. }
  13. std::cout << "Sender: Data is ready." << std::endl;
  14. condVar.notify_one();
  15. }
  • future/promise
  1. void waitingForWork(std::future<void>&& fut){
  2. std::cout << "Worker: Waiting for work." << std::endl;
  3. fut.wait();
  4. doTheWork();
  5. std::cout << "Work done." << std::endl;
  6. }
  7. void setDataReady(std::promise<void>&& prom){
  8. std::cout << "Sender: Data is ready." << std::endl;
  9. prom.set_value();
  10. }

拉原则

线程也可以主动地要求改变状态,而不是被动地等待状态改变。C++中并不支持“拉原则”,但可以用原子变量来实现。

  1. std::vector<int> mySharedWork;
  2. std::mutex mutex_;
  3. std::condition_variable condVar;
  4. bool dataReady{false};
  5. void waitingForWork(){
  6. std::cout << "Waiting " << std::endl;
  7. std::unique_lock<std::mutex> lck(mutex_);
  8. condVar.wait(lck, []{ return dataReady; });
  9. mySharedWork[1] = 2;
  10. std::cout << "Work done " << std::endl;
  11. }
  12. void setDataReady(){
  13. mySharedWork = {1, 0, 3};
  14. {
  15. std::lock_guard<std::mutex> lck(mutex_);
  16. dataReady = true;
  17. }
  18. std::cout << "Data prepared" << std::endl;
  19. condVar.notify_one();
  20. }

有或无时限的等待

条件变量和future有三个用于等待的方法:waitwait_forwait_untilwait_for需要一个时间段,wait_until需要一个时间点。

各种等待策略中,消费者线程等待时间为steady_clock::now() + dur。如果promise已经准备好了,就会获取值;如果没准备好,则只显示其id: this_thread::get_it()

  1. void producer(promise<int>&& prom){
  2. cout << "PRODUCING THE VALUE 2011\n\n";
  3. this_thread::sleep_for(seconds(5));
  4. prom.set_value(2011);
  5. }
  6. void consumer(shared_future<int> fut,
  7. steady_clock::duration dur){
  8. const auto start = steady_clock::now();
  9. future_status status= fut.wait_until(steady_clock::now() + dur);
  10. if ( status == future_status::ready ){
  11. lock_guard<mutex> lockCout(coutMutex);
  12. cout << this_thread::get_id() << " ready => Result: " << fut.get()
  13. << endl;
  14. }
  15. else{
  16. lock_guard<mutex> lockCout(coutMutex);
  17. cout << this_thread::get_id() << " stopped waiting." << endl;
  18. }
  19. const auto end= steady_clock::now();
  20. lock_guard<mutex> lockCout(coutMutex);
  21. cout << this_thread::get_id() << " waiting time: "
  22. << getDifference(start,end) << " ms" << endl;
  23. }

通知一个或所有等待线程

notify_one可以唤醒一个等待的线程,notify_all可以唤醒所有等待的线程。使用notify_one时,不能确定哪一个线程会被唤醒,而其他条件变量则保持在等待状态。因为future和promise之间存在关联性,所以这种情况在std::future中是不可能发生的。如果想模拟一对多的关系,那么应该使用std::shared_future而不是std::future,因为std::shared_future是可以复制的。

下面的程序显示了一个简单的工作流,promise和future之间是一对一/一对多的关系。

  1. // bossWorker.cpp
  2. #include <future>
  3. #include <chrono>
  4. #include <iostream>
  5. #include <random>
  6. #include <string>
  7. #include <thread>
  8. #include <utility>
  9. int getRandomTime(int start, int end) {
  10. std::random_device seed;
  11. std::mt19937 engine(seed());
  12. std::uniform_int_distribution<int> dist(start, end);
  13. return dist(engine);
  14. }
  15. class Worker {
  16. public:
  17. explicit Worker(const std::string& n) :name(n) {}
  18. void operator()(std::promise<void>&& prepareWork,
  19. std::shared_future<void> boss2Worker) {
  20. // prepare the work and notify the boss
  21. int prepareTime = getRandomTime(500, 2000);
  22. std::this_thread::sleep_for(std::chrono::microseconds(prepareTime));
  23. prepareWork.set_value();
  24. std::cout << name << ": " << "Work prepared after "
  25. << prepareTime << " milliseconds." << std::endl;
  26. // still waiting for the permission to start working
  27. boss2Worker.wait();
  28. }
  29. private:
  30. std::string name;
  31. };
  32. int main() {
  33. std::cout << std::endl;
  34. // define the std::promise = > Instruction from the boss
  35. std::promise<void> startWorkPromise;
  36. // get the std::shared_future's from the std::promise
  37. std::shared_future<void> startWorkFuture = startWorkPromise.get_future();
  38. std::promise<void> herbPrepared;
  39. std::future<void> waitForHerb = herbPrepared.get_future();
  40. Worker herb(" Herb");
  41. std::thread herbWork(herb, std::move(herbPrepared), startWorkFuture);
  42. std::promise<void> scottPrepared;
  43. std::future<void> waitForScott = scottPrepared.get_future();
  44. Worker scott(" Scott");
  45. std::thread scottWork(scott, std::move(scottPrepared), startWorkFuture);
  46. std::promise<void> bjarnePrepared;
  47. std::future<void> waitForBjarne = bjarnePrepared.get_future();
  48. Worker bjarne(" Bjarne");
  49. std::thread bjarneWork(bjarne, std::move(bjarnePrepared), startWorkFuture);
  50. std::cout << "BOSS: PREPARE YOUR WORK.\n " << std::endl;
  51. // waiting for the worker
  52. waitForHerb.wait(), waitForScott.wait(), waitForBjarne.wait();
  53. // notify the workers that they should begin to work
  54. std::cout << "\nBOSS: START YOUR WORK. \n" << std::endl;
  55. startWorkPromise.set_value();
  56. herbWork.join();
  57. scottWork.join();
  58. bjarneWork.join();
  59. }

该程序的关键思想是boss(主线程)有三个员工:herb(第53行)、scott(第58行)和bjarne(第63行),每个worker由一个线程表示。老板在第64行等待,直到所有的员工完成工作。这意味着,每个员工在任务下发后的任意时间点,都可以向老板发送完成通知。因为会转到std::future,所以员工到老板的通知是一对一的(第30行)。而从老板到员工的工作指令,则是一对多的通知(第73行)。对于这个一对多的通知,需要使用std::shared_future

处理突变 - 图7

扩展阅读