Skip to content

基于人民邮电出版社出版的C++并发编程实战(第二版), 一个多线程编程的实用性代码仓库

License

Notifications You must be signed in to change notification settings

qmmzzdx/cpp_concurrency_in_action

Repository files navigation

基于C++并发编程实战(第二版), 一个多线程编程的实用性代码仓库

并发编程术语

  • 不变量:多线程中针对某一特定数据的断言,该断言总是成立的,在多线程数据更新时往往会破坏不变量,此时需要进行数据保护防止更新中不变量被破坏
  • 条件竞争:在多线程环境下,操作由两个或多个线程负责,它们争先让自己执行各自的操作,而结果取决于它们执行的相对次序,导致产生了预料之外的执行结果
  • 数据竞争:在多线程环境下,至少有两个线程同时读写同一个共享内存,其中一个线程进行写操作时,其它线程可能会读到非预期的数据
  • 数据自洽:数据之间能够自己验证自己的准确性,并且所有数据准确且符合期望
  • 线程安全的数据结构:多线程执行的操作无论顺序如何,每个线程所见的数据结构都是自洽的,并且数据不会丢失或被破坏,所有不变量终将成立,恶性条件竞争也不会出现
  • 串行化访问:每个线程轮流访问受互斥保护的数据,它们只能先后串行依次访问,而非并发访问
  • 无阻碍性(obstruction-free):如果其它线程全都暂停,则目标线程将在有限步骤内完成自己的操作
  • 无锁性(lock-free):如果多个线程共同操作同一份数据,那么在有限步骤内,其中某一线程能够完成自己的操作
  • 无等待性(wait-free):在某份数据上,每个线程经过有限步骤就能完成自己的操作,即便该数据同时被其它线程所操作

两种并行计算编程模型

  • MPI编程:全称为Message Passing Interface,是一种用于编写并行程序的消息传递编程模型,尤其适用于多节点环境。它提供了一组标准化的编程接口,使得程序可以在不同的机器或平台上运行,同时保持良好的可移植性,MPI不是一种特定的语言,而是一组库函数,可以在C/C++等语言中调用
  • OpenMP编程:是用于共享内存并行系统的多处理器程序设计的一套指导性编译处理方案,OpenMp提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。当选择忽略这些pragma,或者编译器不支持OpenMp时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行

并发编程建议总结

一、避免死锁的建议指导

  1. 避免嵌套锁;假如线程已经持有一个锁,就不要尝试获取第二个锁。每个线程最多只能持有唯一一个锁,仅锁的使用本身不可能导致死锁。当需要获取多个锁时,使用std::lock或std::scoped_lock,一次获取全部锁来避免死锁
  2. 避免在持有锁时调用由用户提供的程序接口;程序接口是用户实现的,所以没有办法确定代码实际内容,它可能做任何事情,包括获取锁。在持有锁的情况下,如果用程序接口尝试获取一个锁,就会违反第一个指导意见,并造成死锁(有时这是无法避免的)
  3. 使用固定顺序获取锁;当硬性要求获取两个或两个以上的锁,并且不能使用std::lock或std::scoped_lock来获取锁时,最好在每个线程上,用固定的顺序获取锁,可以定义遍历的顺序,一个线程必须先锁住A才能获取B的锁,在锁住B之后才能获取C的锁
  4. 使用层次锁结构;通过逐渐递减的层级进行加锁,每层只能获取比当前层更低层次的锁,层次锁能保证每个线程加锁时,一定是先按照权重高低来进行加锁,因而避免了死锁

二、并发数据结构设计指导

  • 一、确保数据结构是多线程访问安全的
  1. 若某线程的行为破坏了数据结构的不变量,则必须确保其它任何线程都无法见到该状态
  2. 保持谨慎排除函数接口固有的条件竞争,数据结构提供的操作应该完整独立,而非零散的分解步骤
  3. 一旦程序抛出异常,要注意数据结构的行为,确保不变量不被破坏
  4. 在数据结构的使用过程中,限制锁对象的作用域,尽可能避免嵌套锁,从而将死锁的可能性降至最低
  • 二、确保真正的并发访问(考虑意见)
  1. 考虑操作在锁的范围中能够进行,并且是否允许在锁外执行,扩大访问范围
  2. 考虑数据结构中不同的互斥量能否保护不同的区域
  3. 所有操作都需要同级互斥量的保护吗
  4. 能否对数据结构进行简单的修改,增加并发访问的概率
  5. 考虑如何让序列化访问最小化,让真实并发访问最大化

三、实现无锁数据结构的原则

  • 一、在原型设计中使用std::memory_order_seq_cst次序

当基本操作均正常工作后,我们才放宽内存次序约束,在这种意义上,采用其他内存次序其实是一项优化,需要避免过早实施

  • 二、使用无锁的内存回收方案(三种方法的关键思想都是以某种方式掌握正在访问目标对象的线程数目,仅当该对象完全不被访问的时候,才会被删除)
  1. 暂缓全部删除对象的动作,使用待删链表,等到没有线程访问数据结构的时候,才删除待销毁的对象
  2. 采用风险指针,以辨识特定对象是否正在被某线程访问
  3. 使用引用计数方式来进行对象计数,只要外部环境仍正在访问目标对象,它就不会被删除
  • 三、防范ABA问题

场景简述:在所有涉及CAS的算法中,都要注意防范ABA问题,简而言之就是由于线程一和线程二之间存在时间差,导致线程二执行完之后又在最后把内存改回线程一原来的值,线程一认为和自己的值相同,则又进行了操作,但此时相关的值已非原来的值,若是指针类型,会导致原有数据结构被破坏

解决方法:在原子变量x中引入一个ABA计数器。将变量x和计数器组成单一结构,作为一个整体执行CAS操作。每当它的值被改换,计数器就自增。照此处理,如果别的线程改动了变量x,即便其值看起来与最初一样,CAS操作仍会失败

  • 四、找出忙等循环,协助其他线程

假设按照调度安排,某线程先开始执行,却因另一线程的操作而暂停等待,那么只要我们修改操作的算法,就能让前者先完成全部步骤,从而避免忙等,操作也不会被阻塞,这时可以将非原子变量的数据成员改为原子变量,并采用CAS操作设置其值,但更复杂的数据结构可能会涉及更多修改

About

基于人民邮电出版社出版的C++并发编程实战(第二版), 一个多线程编程的实用性代码仓库

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages