C++ memory order循序渐进(二)—— C++ memory order基本定义和形式化描述所需术语关系详解


上篇博客提到了,编译阶段和运行阶段的重排在各种多核平台上可能会导致非预期的内存序,而线程间的数据依赖只有程序员自己最清楚,也就是是否有必要进行某些数据同步。同步是有开销的,而且不同的硬件平台开销还不一样,减少不必要的同步有时候能够显著提高性能,无论是strong memory model还是weak memory model,因为性能的考量各种主流多核平台都不会保证顺序一致性,要编写lock free代码不可避免地要考虑内存序。

为了能够便捷、统一的在各平台指定内存序,c++11标准开始在C++的语言定义层面上引进memory order,memory_order给程序员提供了一种指定多线程之间如何进行数据同步而不需要在乎硬件具体底层实现的手段,编码者通过指定memory_order告诉编译器需要什么样的内存序,编译器会根据cpu平台类型选用合适的手段来保证对应的同步,从而能够相对便利地写出高效的跨平台的多线程代码。在此之前要指定内存序要么依赖第三方库,要么需要根据运行的平台直接调用相关cpu指令等,会麻烦很多。

作为一个语言层面引入的新特性,为了保证逻辑的严谨,采用了形式化定义的方式,采用采用形式话定义,就不可避免地会涉及到各种术语和关系,而要想尽可能透彻地理解c++的memory order,吃透这些术语和关系是很有必要的,虽然看上去很晦涩和繁琐,这篇博客首先会简单介绍下六种memory_order,随后会重点聊一下这些术语和关系。

1 c++的六种memory_order

cppreference.com上列出了memory_order的定义:
std::memory_order specifies how memory accesses, including regular, non-atomic memory accesses, are to be ordered around an atomic operation。
也就是指定了包括普通的非原子内存访问在内的原子操作周围的内存访问方式。可以看到,c++的memory order提供的是一种通过原子变量限制内存序的手段,也就是说原子变量不光有我们熟知的原子变量的特性,还能限制包括非原子变量在内的各种变量的内存操作,除了依附在原子变量操作的memory order,c++11还引入了 std::atomic_thread_fence,也能达到类似的效果,这个后面再讨论。std::memory_order是一个枚举,一共有六种,下面分别简单介绍下各自的用途和效果,本篇文章不会详细展开。下面的内容主要是以cppreference上std::memory_order的英文文档内容为基础,加上一些自己的理解和描述的润色,可以结合cppreference一起看,虽然cppreference上有中文版,但因为一些术语的翻译和排版的关系,个人觉得看起来很费劲,不建议直接看中文版。

1.1 memory_order_relaxed

这个很好理解,宽松的内存序,指定为这个的所有原子操作就真的仅仅是保证自身的原子性,不会对任何其他变量的读写产生影响。如果确实不需要其他的内存同步,那么这是最好的选择,比如原子计数器。

1.2 memory_order_consume

memory_order_consume适用于load operation,对于采用此内存序的load operation,我们可以称为consume operation,设有一个原子变量M上的consume operation,对周围内存序的影响是:当前线程中该consume operation后的依赖该consume operation读取的值的load 或strore不能被重排到该consume operation前,其他线程中所有对M的release operation及其之前的对数据依赖变量的写入都对当前线程从该consume operation开始往后的操作可见,相比较于下面讲的memory_order_acquire,memory_order_consume只是阻止了之后有依赖关系的重排。绝大部分平台上,这个内存序只会影响到编译器优化,依赖于dependency chain。但实际上很多编译器都没有正确地实现consume,导致等同于acquire。

1.3 memory_order_acquire

memory_order_acquire适用于load operation,对于采用此内存序的load operation,我们可以称为acquire operation,设有一个原子变量M上的acquire operation,对周围内存序的影响是:当前线程中该acquire operation后的load 或strore不能被重排到该acquire operation前,其他线程中所有对M的release operation及其之前的写入都对当前线程从该acquire operation开始往后的操作可见。

1.4 memory_order_release

memory_order_release适用于store operation,对于采用此内存序的store operation,我们可以称为release operation,设有一个原子变量M上的release operation,对周围内存序的影响是:该release operation前的内存读写都不能重排到该release operation之后。并且:

  1. 截止到该release operation的所有内存写入都对另外线程对M的acquire operation以及之后的内存操作可见,这就是release acquire 语义。
  2. 截止到该operation的所有M所依赖的内存写入都对另外线程对M的consume operation以及之后的内存操作可见,这就是release consume语义。

1.5 memory_order_acq_rel

memory_order_acq_rel适用于read-modify-write operation,对于采用此内存序的read-modify-write operation,我们可以称为acq_rel operation,既属于acquire operation 也是release operation. 设有一个原子变量M上的acq_rel operation:自然的,该acq_rel operation之前的内存读写都不能重排到该acq_rel operation之后,该acq_rel operation之后的内存读写都不能重排到该acq_rel operation之前. 其他线程中所有对M的release operation及其之前的写入都对当前线程从该acq_rel operation开始的操作可见,并且截止到该acq_rel operation的所有内存写入都对另外线程对M的acquire operation以及之后的内存操作可见。

1.6 memory_order_seq_cst

memory_order_seq_cst 可以用于 load operation,release operation, read-modify-write operation三种操作,用于 load operation的时候有acquire operation的特性,用于 store operation的时候有release operation的特性, 用于 read-modify-write operation的时候有acq_rel operation的特性,除此之外,有个很重要的附加特性,一个单独全序,也就是所有的线程会观察到一致的内存修改,也就是第一篇博客提到的顺序一致性的强保证。

以上就是六种内存序的定义和效果,单独理解每一个memory_order并不难,真正比较麻烦的是在实际应用中各种内存序组合后的效果,这也是memory order的重点所在,我们利用的就是各种内存序之间的组合效果来保证程序在实际运行时候的正确memory order,这个后面的博客会展开讲,而要理解这部分的前提就是要了解形式化定义中术语描述的各种关系,这也是本篇接下来的内容。

2 形式化定义用到的术语和关系

2.1 Sequenced-before

这个定义的是同一个线程内的一种根据表达式求值顺序来的一种关系,完整的规则定义很复杂,可以参考http://en.cppreference.com/w/cpp/language/eval_order,其中最直观常用的一条规则简单来说如下:每一个完整表达式的值计算和副作用都Sequenced-before于下一个完整表达式的值计算和副作用。从而也就有以分号结束的语语句Sequenced-before于下一个以分号结束的语句,比如:

r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

从而有 C Sequenced-before D。

2.2 Carries dependency

Carries dependency,字面意思是携带依赖,描述的是有sequenced-before关系的求值操作之间的一种依赖关系,对于求值A和求值B,A Carries dependency into B的意思是B对A有依赖,在A sequenced-before B的前提下,如果:

  1. A是B的一个操作数
  2. A写入了某个标量对象(float、int、指针枚举等)M,B从M里读
  3. 求值X依赖A,B依赖X

那么我们说B依赖A。

2.3 Modification order

Modification order,字面意思是修改顺序,这个是针对原子变量的一个概念,对于c++里的原子变量,有个很重要的保证,那就是:对于任意特定的原子变量的所有修改操作都会以一个特定于该原子变量的全序发生,因此也保证了原子变量的操作在各线程之间有如下四个一致性(为了描述简洁下面说的读写都是针对某一个特定的原子变量):

  1. 写-写一致性:如果写A happens-before 写 B ,那么在Modification order中A 比B早出现
  2. 读-读一致性: 如果读A happens-before 读B, 并且如果 A 读到的是来自写X, 那么B读到的要么也是写X的值,要么是Modification order中比X后出现的一个写Y 的值.
  3. 读-写一致性: 如果读A happens-before 写B, 那么A读到的值来自于modification order 中的一个早于B出现的写X
  4. 写-读一致性: 如果一个写X happens-before 读B , 那么B读取的值可能来自写X或者modification order 中在X之后出现的写Y。
    happens-before是内存序里很重要的一种关系,下面会详细阐述,这里只需要记住,如果A Happens-before B,那么A对内存的修改将会在B操作之前对B可见。上面四条规则,看上去很复杂和繁琐,但阐述的内容其实很好理解。

2.4 Release sequence

Release sequence,指的是某个原子变量M上由一个release operation开始的一个序列,正式定义如下:

原子变量M上的release sequence headed by A指的是M的Modification order上的一个最大连续子序列,该序列由A开始,当前线程内对M的写操作,以及其他线程对M的read-modify-write操作组成。

对应的,有一个比较重要的release sequence rule,能够保证特定情况下多个acquire线程对单个release操作后面展开讲release acquire语义的时候会详细讲下,暂时了解Release sequence这个概念就行。

2.5 Synchronizes-with

Synchronizes-with,也就是同步关系,用来描述对内存的修改操作(包括原子和非原子操作)对其他线程可见,有一点需要注意,这是一个运行时的关系,也就是说不是代码层面的,代码只是让程序在运行的时候有可能建立这种关系,并且一旦建立了就有可见性的保证。简单来说,如果A和B建立了Synchronizes-with关系,那么A之前的内存修改对B之后都可见,preshing大神给的例子很好,如下分别是建立了Synchronizes-with关系和没建立的情况:
在这里插入图片描述
在这里插入图片描述
下图中描述的运行情况是g_guard值在thread1中写入比thread2 中的读取晚,那么就没有建立Synchronizes-With关系,thread1中前面的内存不保证对thread2后面的操作可见。

在Synchronizes-With关系的实际应用中,往往有两个关键元素,用于建立Synchronizes-with关系的原子变量和需要在线程间共享的数据,比如上面图里的g_guard和g_payload,当然guard本身也可以是需要共享的数据,比如队列的top和bottom。

这里先提一嘴,release 和 acquire就能够建立Synchronizes-with关系。

2.6 Dependency-ordered before

这个关系是针对consume来的,对于分属不同线程的赋值A和赋值B,如果他们之间有以下关系之一:

  1. A对某个原子变量M做release操作,B对M做consume操作,并且B读到了release sequence headed by A中的任意一个值。这条就是上面说的release sequence rule的关键。
  2. A is dependency-ordered before X,并且 X carries dependency into B。前面说过了,carries dependency into是线程内的一种关系,这里就是X和B属于同一线程,并且B对X有依赖。

我们就说 A dependency-ordered before B。

2.7 Inter-thread happens-before

Inter-thread happens-before,看字面意思就知道是定义了线程间赋值操作之间的关系,假如有线程1中的赋值操作A,线程2中的赋值操作B,如果满足以下任意一条,那么有A inter-thread happens before B:

  1. A synchronizes-with B
  2. A dependency-ordered before B
  3. A synchronizes-with 某个 evaluation X, 并且 X sequenced-before B
  4. A sequenced-before 某个 evaluation X, and X inter-thread happens-before B
  5. A inter-thread happens-before 某个evaluation X,并且X inter-thread happens-before B

2.8 Happens-before

Happens-before是特别重要的一个概念,定义的是两个赋值之间的一种关系,不管是线程内部还是线程之间,赋值A和赋值B如果满足下面两者条件之一,我们就说A Happens-before B:

  1. A sequenced-before B
  2. A inter-thread happens before B

换句话说,如果A和B处于一个线程内部,只要A sequenced-before B那么就满足A Happens-before B,如果A和B分属两个线程,那么如果有A inter-thread happens before B这层关系那么就满足A Happens-before B。

重点来了,如果A Happens-before B,那么A对内存的修改将会在B操作之前对B可见,我们利用memory_order就是为了确保某些变量之间有Happens-before关系。

2.9 Visible side-effects

可见副作用,通俗地讲就是写能被读看见,正式定义如下:
有对同一标量对象(float、int、指针枚举等)M的写A和读B,如果满足同时满足下述两个条件那么A的副作用对B可见:

  1. A happens-before B。
  2. 不存在一个对M有副作用的写X, A happens-before X 并且X happens-before B。

换句话说,就是A和B有happens-before 关系,并且二者之间没有其他的写,否则会读到其他写的内容,A的副作用就不可见了。

参考:
https://en.cppreference.com/w/cpp/atomic/memory_order
https://preshing.com

  • 5
    点赞
  • 11
    评论
  • 14
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

<p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;">1、系统全面介绍了Python的基础语法 </span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;">2、在课程中融入了算法思想 </span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;">3、帮助初学者厘清逻辑,掌握Python的主体脉络 </span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;">4、从全方位立体角度解析知识点 </span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;">5、实战案例驱动、课程包含近200个相关案例、边讲解边实操</span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;"><br /> </span> </p> <p> <span style="font-family:-apple-system, system-ui, 'PingFang SC', Helvetica, Tahoma, Arial, 'Microsoft YaHei', 微软雅黑, 黑体, Heiti, sans-serif, SimSun, 宋体, serif;font-size:12px;background-color:#ffffff;"><img src="https://img-bss.csdnimg.cn/202107120808123109.png" alt="" /><br /> </span> </p>
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值