本篇笔记包含 Term 35~42。
Term 35:优先选用基于任务而非基于线程的程序设计
std::thread
的 API 并未提供直接获取异步运算函数返回值的途径,而且如果函数出现异常,程序会终止- 基于线程的程序设计要求手动管理处理线程资源耗尽、超订、负载均衡、跨平台等问题
- 使用 async 可以很大程度避免 2. 中的问题
Term 36:如果异步是必要的则指定 std::launch::async
std::async
的默认启动策略不保证任务以异步方式执行-
- 中的方式可能导致线程局部变量不确定,任务永远无法完成(deferred 方式执行)等问题
- 指定
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 析构函数行为保持关注
- future 的正常析构⾏为就是销毁 future 本⾝的成员数据
- 最后⼀个引⽤
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
std::atomic
用于多线程访问的数据,且不用于互斥量volatile
用于读写操作不可以被优化掉的内存,它会让编译器执行每一次内存操纵,而不能优化省略掉中间步骤(例如几条临近的语句都对同一个变量进行赋值的情形)
Term 41:针对可复制的形参,在移动成本低且一定会被复制的前提下,考虑将其按值传递
- 对于可复制的,移动成本低的,而且一定会被复制的形参而言,按值传递和按引用传递效率接近,而且按值传递目标代码更少
- copy 构造函数拷贝形参可能比赋值运算符拷贝形参的成本高出不少
- 按值传递肯定会导致 slicing 问题,所以基类类型不适合按值传递
Term 42:考虑置入而非插入
以 std::vector
为例,置入是 emplace_back,插入是 push_back
- 置入函数有时比插入更高效,且不会有比插入低效的可能
- 置入函数更高效情形的条件:1)待添加物的值是以构造而非赋值方式加入容器;2)传递的实参型别与容器内容物的型别不同;3)容器不会由于存在重复值而拒绝添加
- 置入函数可能会执行在插入函数中会被拒绝的型别转换