1. 优先使用基于任务的编程而非基于线程的编程
这里的意思是,优先使用std::async()
,而非使用std::thread
,来创建异步的任务。
因为大部分情况下,我们只是简单的执行异步任务罢了。
std::async()
的优点:
-
代码简单,能自然获取异步任务结果或者异常
-
异步任务的调度交给了标准库来调度,避免手动管理线程
只有在下面情况下,建议直接使用std::thread
:
-
需要直接访问线程的API
-
需要且能够优化线程的使用
-
需要实现超越线程API的功能
2. 若有异步任务需要,必须指定std::launch::async
std::async()
有2个选项:
-
std::launch::async
:任务必须在不同线程异步执行 -
std::launch::deferred
:任务延迟到future
上调用get/wait
时才执行
默认配置是它们的或值,即std::launch::async | std::launch::deferred
,既不同线程,且延迟执行。
所以,默认情况下,轮询future::wait_for()
可能返回std::future_status::deferred
,且一直如此,所以需要对此进行判断。否则,若任务一定要马上异步执行,一定得显式指定std::launch::async
。
3. 让std::thread
到最后都unjoinable
Unjoinable的线程包括:
-
默认构造
std::thread
,没有任务执行 -
被移动走的
std::thread
-
已经
join
的std::thread
-
已经
detach
的std::thread
:父线程无法再控制子线程
std::thread
销毁时不会隐式join
或detach
,因为可能导致表现异常(如不必要的等待,对于前者)或者未定义行为(悬挂数据等,对于后者):
- 所以,销毁unjoinable的
std::thread
,会导致程序直接中止。
所以必须保证std::thread
销毁前,必须unjoinable。
此外,尽量把std::thread
放在成员变量的最后,保证之前的成员都已经初始化完成后,再初始化线程。
4. 关注不同线程句柄的析构行为
上节说,销毁joinable的std::thread
直接导致程序中止。
而销毁joinable的std::future
不会导致程序中止,因为它只销毁std::future
本身:
-
调用的结果放置在一块共享区域内
-
若存在调用方,
std::shared_future
难以实现 -
若存在被调用方,由于结果是局部的,被调用方被销毁时,结果会被销毁
-
所以放在共享区域内
-
-
若引用了共享状态,那么销毁时,会被阻塞住,等同于隐式
join
5. 对于单次事件通信,使用void
的future
A任务做完后,通知B任务继续做。
一种实现方式是std::condition_variable
,但要注意:
-
需要获取一个锁,配套条件变量使用
-
注意虚假唤醒,即
wait()
需要传递一个Lambda函数,判断条件为真后,才能被唤醒,即如同下面的处理:1
cond.wait([]() { return ready; });
上面的代码等同于循环轮询,以避免虚假唤醒,如下所示:
1 2 3
while (!ready) { cond.wait(); }
另一种实现方式是使用std::future
和std::promise
,B等待A完成的信号:
-
A完成后,通过
std::promise
来设置一个信号,调用set_value()
-
B等待,需要通过
std::promise
调用get_future().wait()
等待信号
上述特点:
-
是1P1C的模型,仅适用于一次通信
-
使用了共享存储状态,它存储在堆中,有开销
-
由于是一次通信,
std::future
和std::promise
的模板参数可以是void
。
6. 并发使用std::atomic
,特殊内存使用volatile
C++std::atomic
等同于Java的Atomic
类,数据操作是原子的。
std::atomic
不支持拷贝和移动,需要通过load()
和store()
传递值
C++volatile
和Javavolatile
不同:
-
C++:没有并发含义,只是告诉编译器它是特殊内存(例如外部设备等),不要优化它的读写,例如
-
冗余多次读取同一个变量,不要优化为读1次
-
冗余多次写如同一个变量,不要优化为写1次
auto y = x
的auto
会把volatile
拿掉(同样const
也会拿掉,若x
是非引用非指针) -
-
Java:保证数据各线程立即可见,采用内存屏障实现,避免了指令重排
- C++的
std::atomic
也能做到这点
- C++的
总之:
-
std::atomic
对并发有用,对特殊内存没用 -
volatile
对特殊内存有用,对并发没用