Effective Modern C++ 笔记 - 并发API
C++11
的伟大成就之一是将并发性合并到语言和库中。熟悉其他线程API(如pthreads
或Windows threads
)的程序员有时会对C++
提供的相对简单的特性集感到惊讶,那是因为C++
对并发性的大量支持是以对编译器的约束的形式出现的。由此产生的语言保证意味着,在C++
的历史上,程序员首次可以编写具有跨所有平台的标准行为的多线程程序。
在下面的条目中,要记住标准库有两个future
模板:std::future
和std::shared_future
。在很多情况下,这种区别并不重要,所以下面说到的feature
同时指的这两种。
35. 优先基于任务编程, 而不是线程
1 | int doAsyncWork(); |
std::thread
不能直接得到返回值, 如果线程函数抛出异常, 程序就终止了- 基于线程编程要考虑系统载荷, 负载平衡, 适应平台等
- 基于任务编程通过
std::async
和启动策略解决了上面的问题 - 对于GUI线程, 可以传递
std::launch::async
策略确保在不同线程执行
36. 如果异步是必需的, 就指定std::launch::async
标准库里有两个启动策略
std::launch::async
在不同线程启动std::launch::deferred
只有调用get
或wait
时才启动, 如果不调用就不会执行- 默认策略是两个策略的组合
1
2
3// 下面两个效果相同
auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);坑
1
2
3
4
5
6
7
8
9
10
11
12
13
14using namespace std::literals;
void f()
{
std::this_thread::sleep_for(1s);
}
auto fut = std::async(f);
// 如果是deferred, 状态永远不会ready
while(fut.wait_for(100ms) != std::future_status::ready)
{
//...
}当默认启动策略选择延迟启动时, 由于只有调用
get
或wait
才能启动,wait_for
和wait_until
是不会启动的, 上面的代码就会死循环.结论
- 默认的启动策略既可能是异步执行也可能是同步执行
- 如果必须要异步, 就指定
std::launch::async
37. 确保析构std::thread
时状态是unjoinable
状态
下面几种情形就是unjoinable状态的std::thread
:
- 默认构造的
std::thread
对象 - 已经被
move
了 - 已经
join
过了 - 已经被
detach
了
当std::thread
析构时, 如果不是unjoinable
状态, 程序将被终止。
另外,如果类里有std::thread
对象的话, 把它的声明放到所有成员的最后面。因为C++构造对象时, 其成员的构造顺序是按声明顺序做的, 析构则正相反。我们要确保线程启动时其它成员数据已就绪。
38. 对变化多端的线程句柄析构函数行为保持关注
- 如果
std::future
来自于以std::launch::async
策略启动的std::async
, 并且其是最后一个引用, 那么它析构时会有隐含的join
操作阻塞直到任务完成。 - 其它情况,它可能会是
detach
也可能是什么都不做。
39:针对一次性的事件触发用std::future<void>
假设要让线程启动后先暂停一段时间,等其它资源准备完毕再继续,我们有下面的方案:
方案一: 用condition_variable
1 | std::condition_variable cv; |
1 | // 线程 |
方案二: 用std::future<void>
1 | std::promise<void> p; |
1 | // 线程 |
第二个方案显然更简单,只是它只能用于一次性的触发,其它情况还是要用
condition_variable
40:对并发使用std::atomic,对特殊内存使用volatile
记得以前学C++的时候有书说volatile
是用来告诉编译器不要优化,因为可能会被其它线程修改。实际上:
- 如果用于线程,应该用
std::atomic
。 volatile
用在特殊内存上,特殊内存指DMA
、外部传感器
、网络端口
等会被程序外因素修改的内存。