avatar

Catalog
Effective Modern C++ 笔记(7):The Concurrency API

Chapter 7 The Concurrency API

Item 35 Prefer task-based programming to thread-based

  • 异步执行一个函数有两种方法:std::threadstd::async

    c++
    1
    2
    3
    4
    5
    int doAsyncWork();

    std::thread t(doAsyncWork);

    auto fut = std::async(doAsyncWork);
  • async 相比 thread 有如下优点:

    • 能更方便地获取返回值、捕获异常;
    • 使用默认的 policy 可以将线程管理的任务交给系统。

Item 36 Specify std::launch::async if asynchronicity is essential

  • std::async 有两种 policy:asyncdeferred,前者确保异步执行,后者推迟执行直到调用 async 的线程调用了 getwait。如果不指定 policy,则系统会根据负载自动指定。

  • 让系统来指定的话会有一些问题:

    • 由于没有办法确定函数是不是在一个新的线程中执行,所以当函数读写一些 thread local 的变量时需要小心;
    • 如果系统指定 deferred,那么 wait_for 的返回值也会是 deferred,需要将这一点考虑进来;
    • 如果系统指定 deferred,并且没有 getwait,那么函数不会被执行。

    必要的话需要手动指定 policy 为 async

Item 37 Make std::threads unjoinable on all paths

  • std::thread 有两个状态:joinable 和 unjoinable

    • joinable 是说 std::thread 和一个线程相对应,这个线程可以是处于等待执行、正在执行、阻塞或终止的状态;
    • unjoinable 是说 std::thread 不和任何一个线程相对应,有以下一些情况会导致一个 std::thread unjoinable:
      • 默认构造一个 std::thread,没有传进去一个函数。std::thread 没有东西执行,自然就不会绑定到任何一个线程;
      • 被移动的 std::thread,原先和该 std::thread 绑定的线程被移动到了另一个 std::threadstd::thread 禁用拷贝);
      • 已经被 joinstd::thread。调用 join 方法会等待函数执行完成并回收线程;
      • 已经被 detachstd::thread。调用 detach 方法强行断开 std::thread 和某个线程的关联。
  • 如果一个 joinable std::thread 的析构函数被调用,程序将会终止。因为程序无法决定使用隐式的 join 还是隐式的 detach

    • 如果使用隐式的 join,会带来性能上的问题。因为析构 std::thread 意味着其中执行的函数已经不再重要,再花费时间等待其执行完成就没有必要了。
    • 如果使用隐式的 detach,则会带来正确性上的问题。因为 std::thread 在执行时往往会修改它存在的栈帧中的数据,而直接将其 detach 然后继续执行主线程就有可能导致主线程的下一个栈帧和之前 std::thread 的栈帧有重合部分,主线程中的数据就好像是莫名其妙被修改了一样。

    好的解决方法是通知 std::thread 立刻停止执行其中的函数,但是 C++11 并不支持这一点。

  • 程序员应当确保在任何一条执行路径中,std::thread 最终处于 unjoinable 的状态。而手动保证这一点是很难的,但是可以借用 RAII 的概念,在 std::thread 上封装一个类,在析构函数中将 std::thread 的状态改变成 unjoinable。

Item 38 Be aware of varing thread handle destructor behavior

  • async 的 task 和 std::thread 一样,都和一个线程相对应,它们称为 thread handle(线程句柄)
  • 但是它们析构时的行为不太一样,future 析构时,大部分情况下就是直接析构,不 join 也不 detach,只是将 shared state 的 reference count 减一;而当满足以下三个条件时,析构会阻塞住直到 task 执行完(就像被调了 join 一样):
    • 这个 futurestd::async 创建的 shared state 相关联;
    • std::async 的 policy 是 async(不管是指定的还是系统选择的);
    • 这个 future 是和这个 shared state 相关联的最后一个 future

Item 39 Consider void futures for one-shot event communication

  • 使用 condition variable 和 flag 可以实现同步机制(detect - react,某一线程达到某个条件时,另一线程才能继续执行),但是实现的方式并不优雅(需要互斥锁、需要防止假醒等等)

  • 使用 promisefuture 能达到类似的效果,但是缺点是只能做一次同步:

    c++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    std::promise<void> p;
    void react(); // func for reacting task
    void detect() // func for detecting task
    {
    std::thread t([] {
    p.get_future().wait(); // suspend
    react();
    });
    p.set_value(); // awake
    t.join();
    }

    使用 shared_future 可以一次通知多个线程。

Item 40 Use std::atomic for concurrency, volatile for special memory

  • std::atomic 提供一种原子操作的手段,对 atomic 变量的 RMW(Read - Modify - Write)操作是原子的;此外它还提供一种类似 barrier 的机制,即在 atomic 操作前的语句不会被 reorder 到 atomic 操作后。
  • volatile 则是用来告诉编译器某块内存是特殊的,比如用于 memory-mapped I/O 的内存(和外围设备通信),这就决定了编译器对这些内存的读写操作不能做像正常内存那样的优化(比如多次读就合并为一次,多次写就只写最后一次)。

Reference

Author: Gusabary
Link: http://gusabary.cn/2020/05/26/Effective-Modern-C++-Notes/Effective-Modern-C++-Notes(7)-The-Concurrency-API/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment