0

Effective Modern C++(9): 并发

 1 year ago
source link: https://keys961.github.io/2022/06/15/Effective-Modern-C++(9)/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

  • 已经joinstd::thread

  • 已经detachstd::thread:父线程无法再控制子线程

std::thread销毁时不会隐式joindetach,因为可能导致表现异常(如不必要的等待,对于前者)或者未定义行为(悬挂数据等,对于后者):

  • 所以,销毁unjoinable的std::thread,会导致程序直接中止。

所以必须保证std::thread销毁前,必须unjoinable。

此外,尽量把std::thread放在成员变量的最后,保证之前的成员都已经初始化完成后,再初始化线程。

4. 关注不同线程句柄的析构行为

上节说,销毁joinable的std::thread直接导致程序中止。

而销毁joinable的std::future不会导致程序中止,因为它只销毁std::future本身:

  • 调用的结果放置在一块共享区域内

    item38_fig2
    • 若存在调用方,std::shared_future难以实现

    • 若存在被调用方,由于结果是局部的,被调用方被销毁时,结果会被销毁

    • 所以放在共享区域内

  • 若引用了共享状态,那么销毁时,会被阻塞住,等同于隐式join

5. 对于单次事件通信,使用void的future

A任务做完后,通知B任务继续做。

一种实现方式是std::condition_variable,但要注意:

  • 需要获取一个锁,配套条件变量使用

  • 注意虚假唤醒,即wait()需要传递一个Lambda函数,判断条件为真后,才能被唤醒,即如同下面的处理:

    cond.wait([]() { return ready; });
    

    上面的代码等同于循环轮询,以避免虚假唤醒,如下所示:

    while (!ready) {
      cond.wait();
    }
    

另一种实现方式是使用std::futurestd::promise,B等待A完成的信号:

  • A完成后,通过std::promise来设置一个信号,调用set_value()

  • B等待,需要通过std::promise调用get_future().wait()等待信号

上述特点:

  • 是1P1C的模型,仅适用于一次通信

  • 使用了共享存储状态,它存储在堆中,有开销

  • 由于是一次通信,std::futurestd::promise的模板参数可以是void

6. 并发使用std::atomic,特殊内存使用volatile

C++std::atomic等同于Java的Atomic类,数据操作是原子的。

  • std::atomic不支持拷贝和移动,需要通过load()store()传递值

C++volatile和Javavolatile不同:

  • C++:没有并发含义,只是告诉编译器它是特殊内存(例如外部设备等),不要优化它的读写,例如

    • 冗余多次读取同一个变量,不要优化为读1次

    • 冗余多次写如同一个变量,不要优化为写1次

    auto y = xauto会把volatile拿掉(同样const也会拿掉,若x是非引用非指针)

  • Java:保证数据各线程立即可见,采用内存屏障实现,避免了指令重排

    • C++的std::atomic也能做到这点
  • std::atomic对并发有用,对特殊内存没用

  • volatile对特殊内存有用,对并发没用


Related Issues not found

Please contact @keys961 to initialize the comment


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK