《C++ 并发编程实战 第二版》前 4 章 标准库工具及其使用:思维导图

《C++ 并发编程实战 第二版》前 4 章 标准库工具及其使用:思维导图

推荐阅读

《C++ 并发编程实战 第二版》学习笔记目录

下面是 markdown 版本,思维导图只是对书中内容主题的粗略概括,部分知识点的详细研究请关注学习笔记目录当中的相关文章

第 1 章 你好,C++ 并发世界

1.1 什么是并发

  • 单核
  • 多核

1.2 为什么要并发

  • 分离
  • 性能

1.3 并发方式

  • 多进程
  • 多线程

第 2 章 线程管理

2.1 线程管理基础

启动线程 std::thread

  • 可以用可调用类型构造
    • 普通函数
    • 仿函数对象
    • Lambda 表达式
    • 非静态成员函数:有时候需要传入对象的指针或者地址
    • 静态成员函数:不需要传入对象,只需要传入类函数地址
  • 构造函数传入临时变量可能导致“最令人头痛的语法解析”
    • 多组括号
    • {} 初始化
    • Lambda
  • 不可拷贝

在线程对象销毁前决定

  • 线程分离 detach
  • 线程等待
    • join
      • RAII + join:join 前判断 joinable,局部变量的销毁是逆序销毁的
      • 不可重复 join
      • 不能对没有执行线程的 std::thread 使用,可以用 joinable 检查
    • 更灵活的
      • 条件变量
      • 期物
  • 销毁前没决定:std::thread 析构函数调用 std::terminate(),触发相应的异常

后台运行线程:使用 detach 分离线程

  • 可能的问题:局部变量线程结束已经销毁,但别的线程还在运行还要使用这个局部变量
  • 分离后 std::thread 对象和实际执行的线程无关了
  • 分离的线程无法加入
  • 不能对没有执行线程的 std::thread 使用,可以用 joinable 检查
  • 在detach下要避免隐式转换,因为此时子线程可能还来不及转换主线程就结束了,应该在构造线程时,用参数构造一个临时对象传入

2.2 向线程函数传递参数

  • std::thread的构造函数只会单纯的拷贝传入的变量
  • 特别需要注意的是传递引用时,传入的是值的副本,也就是说子线程中的修改影响不了主线程中的值。可以使用 std::ref 解决
  • 传递指针
    • 主线程和子线程中的指针都是指向同一块内存。所以在这种情况下会有一个陷阱,如果使用detach(),则当主线程崩溃或者正常结束后,该块内存被回收,若此时子线程没有结束,那么子线程中指针的访问将未定义,程序会出错
  • 传递临时对象
    • 用临时变量作为实参时,会更高效,由于临时变量会隐式自动进行移动操作,这就减少了整体构造函数的调用次数。而一个命名变量的移动操作就需要std::move()

2.3 转移线程所有权

  • 执行线程的所有权可以在 std::thread 实例之间移动
  • 如果所有者是一个临时对象,移动操作将会被隐式调用

2.4 运行时决定线程数量

std::thread::hardware_concurrency()

2.5 标识线程

std::thread::id
get_id() 获取

第 3 章 线程间共享数据

3.1 共享数据带来的问题

  • 条件竞争
  • 避免恶性条件竞争
    • 对数据结构采用保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态(互斥量)
    • 对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化(无锁编程)
    • 使用事务的方式去处理数据结构的更新

3.2 使用互斥量保护共享数据

RAII 互斥锁

检查指针和引用(可能可以绕过保护机制)

  • 没有成员函数通过返回值或传出参数方式向调用者返回受保护数据的指针或引用
  • 检查成员函数是否通过指针或引用的方式来调用
  • 只是将所有可访问数据结构的代码标记为互斥基本等于没保护,依赖于开发者正确使用
  • 切勿将受保护的指针或引用传递到互斥锁作用域之外

定位接口间的条件竞争

  • 问题可能发生在接口设计上而非实现上,需要改变接口设计
  • 削减接口可以获得最大程度的安全,有时候甚至需要限制原来的一些操作

避免死锁的建议和策略

  • 避免嵌套锁
  • 避免在持有锁的时候调用用户提供的代码
  • 使用固定顺序获取锁(ABBA死锁)
    • 定义遍历的顺序,一个线程必须先锁住 A 才能获取 B 的锁,在锁住 B 之后才能获取 C 的锁
    • std::lock 内部使用死锁避免算法,可以同时锁著多个互斥量,可以搭配 lock_guard 使用(scoped_lock)
  • 使用锁的层次结构

std::unique_lock 灵活的锁

  • 支持 lock、try_lock、unlock,可以自由决定啥时候上锁、啥时候解锁
  • 比 lock_guard 灵活,但对象的体积更大了
  • 可以移动但不可赋值

锁的粒度

3.3 保护共享数据的可选方式

保护共享数据的初始化过程

  • 延迟初始化技巧
  • 声名狼藉的双重检查锁定模式:指令重排
  • call_once 和 once_flag:mutex 和 onece_flag 的实例不能拷贝和移动
  • 在只需要一个全局实例的情况下(单例模式),可以利用 C++11 局部 static 特性来替代 call_once

保护不常更新的数据结构(读多写少)

  • 读写锁:C++17 shared_mutex 和 shared_timed_mutex
  • 性能
    • 依赖于参与其中的处理器数量
    • 同样也与读者和写者线程的负载有关
  • 当任意线程拥有一个共享锁时,这个线程会尝试获取一个读写锁,直到其他线程放弃他们的锁
  • 当任意线程拥有一个独占锁时,其他线程无法获得共享锁或独占锁,直到第一个线程放弃其所拥有的锁
  • 嵌套锁:不推荐使用

第 4 章 同步并发操作

4.1 等待一个事件或其他条件

选择

  • 自旋,持续检查
  • 在检查间隙间歇性睡眠
  • 条件变量

等待条件达成

  • condition_variable
  • wait
    • 配合灵活的 unique_lock 而不是 lock_guard
    • 条件满足时从wait 返回并继续持有锁,不满足的时候线程对互斥量解锁,并重新开始等待

4.2 使用期望等待一次性事件

  • 一次性事件 future
  • async 函数模板:后台任务返回值
  • packaged_task 任务与期望值关联:打包任务,可以在线程间传递任务
  • promises
  • 将异常存与期望值中
  • 多个线程的等待 std::shared_future:让每个线程都拥有自己的 shared_future 拷贝对象,这样就可以让多线程访问共享同步结果安全

4.3 限时等待事件

  • 时钟类
  • 时延
  • 时间点

4.4 使用同步操作简化代码

  • 使用期望值的函数化编程
  • 使用消息传递的同步操作
  • CSP
  • Actor model
  • 并发技术扩展规范中的持续性并发:持续性连接
  • 等待多个期望值
    • when_all
    • when_any 等待第一个期望值
  • 并发技术扩展规范中的锁存器和栅栏机制

你可能感兴趣的:(C++,c++,开发语言,多线程,并发)