本篇笔记包含 Term 35~42。

Term 35:优先选用基于任务而非基于线程的程序设计

  1. std::thread 的 API 并未提供直接获取异步运算函数返回值的途径,而且如果函数出现异常,程序会终止
  2. 基于线程的程序设计要求手动管理处理线程资源耗尽、超订、负载均衡、跨平台等问题
  3. 使用 async 可以很大程度避免 2. 中的问题

Term 36:如果异步是必要的则指定 std::launch::async

  1. std::async 的默认启动策略不保证任务以异步方式执行
    1. 中的方式可能导致线程局部变量不确定,任务永远无法完成(deferred 方式执行)等问题
  2. 指定 std::async 的启动策略可以避免上述问题。auto fut = std::async(std::launch::async | std::launch::deferred, f);

Term 37:使 std::tread 型别对象在所有路径都 unjionable

unjionable 状态表示该 std::tread 处于安全结束的状态,在编写多线程程序时应当达到这个要求。然而考虑所有路径的情况是复杂的,包括 return、break、goto、异常跳出等多种情况,合适的方法是使用 RAII 对象来管理。

class ThreadRAII {
 public:
  enum class DtorAction { join, detach };
  ThreadRAII(std::thread&& t, DtorAction a): action(a), t(std::move(t)) {}
  ~ThreadRAII() {
    if (t.joinable()) {
      if (action == DtorAction::join) {
        t.join();
      } else {
        t.detach();
      }
    }
  }
  ThreadRAII(ThreadRAII&&) = default;
  ThreadRAII& operator=(ThreadRAII&&) = default;
  std::thread& get() { return t; };
 private:
  DtorAction action;
  std::thread t;
};

Term 38:对变化多端的线程 handle 析构函数行为保持关注

  1. future 的正常析构⾏为就是销毁 future 本⾝的成员数据
  2. 最后⼀个引⽤ std::async 创建的共享状态的 future 的析构函数会在任务结束前保持阻塞

Term 39:考虑针对一次性事件通信使用以 void 为模板型别实参的 future

std::promise<void> p;
void detect() {
  auto sf = g.get_future().share();  // sf 的型别是 std::shared_future<void>
  std::vector<std::thread> vt;
  for (int i = 0; i < threadsToRun; ++i) {
    vt.emplace_back([sf] {
      sf.wait();
      react();
    });
  }
  ...  // 若此段省略内容抛出异常,detect 函数会失去响应
  p.set_value();  // 让所有线程取消暂停
  ...
  for (auto& t : vt) {  // make all threads unjoinable
    t.join();
  }
}

Term 40:对并发使用 std::atomic,对特种内存使用 volatile

  1. std::atomic 用于多线程访问的数据,且不用于互斥量
  2. volatile 用于读写操作不可以被优化掉的内存,它会让编译器执行每一次内存操纵,而不能优化省略掉中间步骤(例如几条临近的语句都对同一个变量进行赋值的情形)

Term 41:针对可复制的形参,在移动成本低且一定会被复制的前提下,考虑将其按值传递

  1. 对于可复制的,移动成本低的,而且一定会被复制的形参而言,按值传递和按引用传递效率接近,而且按值传递目标代码更少
  2. copy 构造函数拷贝形参可能比赋值运算符拷贝形参的成本高出不少
  3. 按值传递肯定会导致 slicing 问题,所以基类类型不适合按值传递

Term 42:考虑置入而非插入

std::vector 为例,置入是 emplace_back,插入是 push_back

  1. 置入函数有时比插入更高效,且不会有比插入低效的可能
  2. 置入函数更高效情形的条件:1)待添加物的值是以构造而非赋值方式加入容器;2)传递的实参型别与容器内容物的型别不同;3)容器不会由于存在重复值而拒绝添加
  3. 置入函数可能会执行在插入函数中会被拒绝的型别转换