Effective Modern C++ 笔记 - 并发API

C++11的伟大成就之一是将并发性合并到语言和库中。熟悉其他线程API(如pthreadsWindows threads)的程序员有时会对C++提供的相对简单的特性集感到惊讶,那是因为C++对并发性的大量支持是以对编译器的约束的形式出现的。由此产生的语言保证意味着,在C++的历史上,程序员首次可以编写具有跨所有平台的标准行为的多线程程序。

在下面的条目中,要记住标准库有两个future模板:std::futurestd::shared_future。在很多情况下,这种区别并不重要,所以下面说到的feature同时指的这两种。

35. 优先基于任务编程, 而不是线程

1
2
3
4
int doAsyncWork();

// std::thread t(doAsyncWork);
auto fut = std::async(doAsyncWork);
  • std::thread不能直接得到返回值, 如果线程函数抛出异常, 程序就终止了
  • 基于线程编程要考虑系统载荷, 负载平衡, 适应平台等
  • 基于任务编程通过std::async和启动策略解决了上面的问题
  • 对于GUI线程, 可以传递std::launch::async策略确保在不同线程执行

36. 如果异步是必需的, 就指定std::launch::async

标准库里有两个启动策略

  1. std::launch::async 在不同线程启动
  2. std::launch::deferred 只有调用getwait时才启动, 如果不调用就不会执行
  3. 默认策略是两个策略的组合
    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
    14
    using 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)
    {
    //...
    }

    当默认启动策略选择延迟启动时, 由于只有调用getwait才能启动, wait_forwait_until是不会启动的, 上面的代码就会死循环.

    结论

  • 默认的启动策略既可能是异步执行也可能是同步执行
  • 如果必须要异步, 就指定std::launch::async

37. 确保析构std::thread时状态是unjoinable状态

下面几种情形就是unjoinable状态的std::thread:

  • 默认构造的std::thread对象
  • 已经被move
  • 已经join过了
  • 已经被detach

std::thread析构时, 如果不是unjoinable状态, 程序将被终止。
另外,如果类里有std::thread对象的话, 把它的声明放到所有成员的最后面。因为C++构造对象时, 其成员的构造顺序是按声明顺序做的, 析构则正相反。我们要确保线程启动时其它成员数据已就绪。

38. 对变化多端的线程句柄析构函数行为保持关注

  1. 如果std::future来自于以std::launch::async策略启动的std::async, 并且其是最后一个引用, 那么它析构时会有隐含的join操作阻塞直到任务完成。
  2. 其它情况,它可能会是detach也可能是什么都不做。

39:针对一次性的事件触发用std::future<void>

假设要让线程启动后先暂停一段时间,等其它资源准备完毕再继续,我们有下面的方案:
方案一: 用condition_variable

1
2
3
4
5
6
7
8
9
std::condition_variable cv;
std::mutex m;
bool flag(false);
//... 资源准备完毕
{
std::unique_lock<std::mutiex> g(m);
flag = true;
}
cv.notify_one();
1
2
3
4
// 线程
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return flag;});
//...

方案二: 用std::future<void>

1
2
3
std::promise<void> p;
//...资源准备完毕
p.set_value();
1
2
// 线程
p.get_future().wait();

第二个方案显然更简单,只是它只能用于一次性的触发,其它情况还是要用condition_variable

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

记得以前学C++的时候有书说volatile是用来告诉编译器不要优化,因为可能会被其它线程修改。实际上:

  • 如果用于线程,应该用std::atomic
  • volatile用在特殊内存上,特殊内存指DMA外部传感器网络端口等会被程序外因素修改的内存。