如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:

  • 即使是简单的语句,C++ 也不保证是原子操作。
  • CPU 可能会调整指令的执行顺序。
  • 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。

利用 C++ 的 atomic<T> 能完成对象的原子的读、写以及RMW(read-modify-write),而参数 std::memory_order 规定了如何围绕原子对象的操作进行排序。memory order 内存操作顺序其实是 内存一致性模型 (Memory Consistency Model),解决处理器的 write 操作什么时候能够影响到其他处理器,或者说解决其他处理处理器什么时候能够观测到当且 写CPU/写线程 写入内存的值,有了 memory odering,我们就能知道其他处理器是怎么观测到 store 指令的影响的。

一致模型有很多种,在 Wikipedia 里面搜索 Consistency model 即可看到,目前 C++ 所用到有 Sequential Consistency 和 Relaxed Consistency 以及 Release consistency。

Memory Operation Ordering

我们所编写的程序会定义一系列的 loadstore 操作,也就是 Program ordering,这些 load 和 store 的操作应用在内存上就有了内存操作序(memory operation ordering),一共有四种内存操作顺序的限制,不同的内存一致模型需要保持不同级别的操作限制,其中 W 代表写,R 代表读:

  • W -> R:写入内存地址 X 的操作必须比在后面的程序定义序列的读取地址 Y 之前提交 (commit), 以至于当读取内存地址 Y 的时候,写入地址 X 的影响已经能够在读取Y时被观测到。
  • R -> R: 读取内存地址 X 的操作必须在后序序列中的读取内存地址 Y 的操作之前提交。
  • R -> W:读取内存地址 X 的操作必须在后序序列中读取内存地址 Y 的操作之前提交。
  • W -> W:写入内存地址 X 的操作必须在后续序列中写入内存地址 Y 的操作之前提交。

提交的意思可以理解为,后面的操作需要等前面的操作完全执行完才能进行下一个操作。

Sequential consistency

序列一致是 Leslie Lamport 提出来的,如果熟悉分布式共识算法 Paxos ,那么应该不陌生这位大科学家,而序列一致的定义是:

the result of any execution is the same as-if (任何一种执行结果都是相同的就好像)

  1. the operations of all threads are executed in some sequential order (所有线程的操作都在某种次序下执行)
  2. the operations of each thread appear in this sequence in the order specified by their program (在全局序列中的,各个线程内的操作顺序由程序指定的一致)

组合起来:全局序列中的操作序列要和线程所指定的操作顺序要对应,最终的结果是所有线程指定顺序操作的排列,不能出现和程序指定顺序组合不出来的结果。

怎么做会违反 sequcential consistency(SC)?也就是 SC 的反例是什么?

  • 乱序执行 (out-of-order)
  • 内存访问重叠,写A的过程中读取A,宽于计算机word的,64位机器写128位变量

更加形象的理解可以从内存的角度来看:

所有的处理器都按照 program order 发射 loadstore 的操作,而内存一个地一个地从上面 4 个处理器中读取指令,并且仅当完成一个操作后才会去执行下一个操作,类似于多个 producer 一个 consumer 的情况。

(Lamport 一句话,让我为他理解了一下午)

SC 需要保持所有的内存操作序(memory operation ordering),也是最严格的一种,并且 SC 是 c++ atomic<T> 默认的以一种内存模型,对应 std::memory_order_seq_cst,可以看到标准库中的函数定义将其设置为了默认值:

    bool
load(memory_order __m = memory_order_seq_cst) const noexcept
{ return _M_base.load(__m); }

Relaxed Consistency

松弛内存序,对应的 std::memory_order_relaxed,在 cppreference 上的说明是:"不保证同步操作,不会将一定的顺序强加到并发内存访问上,只保证原子性和修改顺序一致性",并且通常用于计数器,比如 shared_ptr 的引用计数。

松弛内存序不再保证 W -> R,不相互依赖的读写操作可以在 write 之前或者在同一时间段并行处理。(读内存并不是想象中的那么简单,有内存寻址过程,将内存数据映射到 cache block,发送不合法位用于缓存替换)

好处是什么?性能,执行命令的写操作的延迟都被抹去了,cpu 能够更快的执行完一段带有读写的指令序列。

具体实现是通过在 cpu 和 cache 之间加入一个 write buffer,如下图:

处理器 Write 命令将会发送到 Write Buffer,而 Read 命令就直接能访问 cache,这样可以省去写操作的延迟。Write Buffer 还有一个细节问题,放开 W -> R 的限制是当 WriteRead 操作内存地址不是同一个的时候,R/W 才能同时进行甚至 R 能提前到 W 之前,但如果 Write Buffer 中有一个 Read 所依赖的内存地址就存在问题,Read 需要等在 Write buffer 中的 Write 执行完成才能继续吗?只需要 Read 能直接访问这个 Write Buffer,如下(注:这里的Load通常和Read等意,StoreWrite等意):

Release Consistency

在这种一致性下,所有的 memory operation ordering 都将不再维护,是最激进的一种内存一致模型,进入临界区叫做 Acquire ,离开临界区叫做 Release。所有的 memory operation ordering 都将不再维护,处理器支持特殊的同步操作,所有的内存访问指令必须在 fence 指令发送之前完成,在 fench 命令完成之前,其他所有的命令都不能开始执行。

Intel x86/x64 芯片在硬件层面提供了 total store ordering 的能力,如果软件要求更高级别的一致性模型,处理器提供了三种指令:

  • mm_lfence:load fence,等待所有 load 完成
  • mm_sfence:store fence,等待所有 store 完成
  • mm_mfence:完全读写屏障

而在 ARM 架构上,提供的是一种非常松弛(very relaxed)内存一致模型。

PS. 曾经有个公司做出了支持 Sequential Consistency 的硬件,但是最终还是败给了市场。

Acquire/Release

Acquire/release 对应 std::memory_order_acquirestd::memory_order_acquire,它们的语义解释如下:

  • Acquire:如果一个操作 X 带有 acquire 语义,那么在操作 X 后的所有 load/store 指令都不会被重排序到操作 X 之前,其他处理器会在看到操作X后序操作的影响之前看到操作 X 的影响,也就是必须先看到 X 的影响,再是后续操作的影响。
  • Relase:如果一个操作 X 带有 release 语义,那么在操作 X 之前的所有 load/store 指令操作都不会被重排序到操作 X 之后,其他处理器会先看到操作 X 之前的操作。

Acquire/Release 常用在互斥锁(mutex lock)和自旋锁(spin lock),获得一个锁和释放一个锁需要分别使用 Acquire 和 Release 语义防止指令操作被重排出临界区,从而造成数据竞争。

Acquire/Consume

Acquire/Consume 对应 std::memory_order_acquirestd::memory_order_consume,两种内存模型的组合仅有 consume 不同于 release,不同点在于,假设原子操作 X, Release 会防止 X 之前的所有指令不会被重排到 X 之后,而 Consume 只能保证依赖的变量不会被重排到 X 之后,引入了依赖关系。

但是在 cppreference 上面写着,“释放消费顺序的规范正在修订中,而且暂时不鼓励使用 memory_order_consume 。”,所以暂时不对其做深入的研究。

Volatile

volatile 关键词通常会被拿出来说,因为通常会在并发编程中被错误使用:

volatile 的翻译是“不稳定的,易发生变化的”,编译器会始终读取 volatile 修饰的变量,不会将变量的值优化掉,但是这不是用在线程同步的工具,而是一种错误行为,cppreference上面写道:“volatile 访问不建立线程间同步,volatile 访问不是原子的,且不排序内存,非 volatile 内存访问可以自由地重排到 volatile 访问前后。”(Visual Studio 是个例外)。

volatile 变量的作用是用在非常规内存上的内存操作,常规内存在处理器不去操作的时候是不会发生变化的,但是像非常规内存如内存映射I/O的内存,实际上是在和外围设备做串口通信,所以不能省去。(《modern effective c++》)

C++ atomic 和 memory ordering 笔记的更多相关文章

  1. Memory Ordering in Modern Microprocessors

    Linux has supported a large number of SMP systems based on a variety of CPUs since the 2.0 kernel. L ...

  2. Multi-core compute cache coherency with a release consistency memory ordering model

    A method includes storing, with a first programmable processor, shared variable data to cache lines ...

  3. memory ordering 内存排序

    Memory ordering - Wikipedia https://en.wikipedia.org/wiki/Memory_ordering https://zh.wikipedia.org/w ...

  4. Satisfying memory ordering requirements between partial reads and non-snoop accesses

    A method and apparatus for preserving memory ordering in a cache coherent link based interconnect in ...

  5. Memory Ordering (注意Cache带来的副作用,每个CPU都有自己的Cache,内存读写不再一定需要真的作内存访问)

    Memory Ordering   Background 很久很久很久以前,CPU忠厚老实,一条一条指令的执行我们给它的程序,规规矩矩的进行计算和内存的存取. 很久很久以前, CPU学会了Out-Of ...

  6. [译]Memory Reordering Caught in the Act

    原文:http://preshing.com/20120515/memory-reordering-caught-in-the-act/ 编写lock-free的C/C++程序时,在保证memory  ...

  7. 还是说Memory Model,gcc的__sync_synchronize真是太坑爹了

    还是说Memory Model,gcc的__sync_synchronize真是太坑爹了! 时间 2012-01-29 03:18:35  IT牛人博客聚合网站 原文  http://www.udpw ...

  8. memory consistency

    目前的计算机系统中,都是shared memory结构,提供统一的控制接口给软件, shared memory结构中,为了memory correctness,可以将问题分为:memory consi ...

  9. boost atomic

    文档: http://www.boost.org/doc/libs/1_53_0/doc/html/atomic.html Presenting Boost.Atomic Boost.Atomic i ...

随机推荐

  1. bzoj3545/bzoj3551 [ONTAK2010]Peaks/Peaks加强版

    bzoj3545/bzoj3551 [ONTAK2010]Peaks/Peaks加强版 传送门:bzoj  bzoj wdnmd为什么加强版不是权限题原题却是啊 3545: [ONTAK2010]Pe ...

  2. Java中实现多态的机制是什么?

    Java允许父类或接口定义的引用变量指向子类或具体实现类的实例对象,而程序调用的方法在运行时才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类 ...

  3. Python使用pip安装No matching distribution found for PyYaml==5.3.1

    ERROR: Command errored out with exit status 1: command: /usr/local/dmahz/p_book_data/bin/python3.9 - ...

  4. SpringMvc怎么和AJAX相互调用的?

    通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象.具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法 ...

  5. JVM组成结构以及各部分的功能

    Java虚拟机主要分为以下五个区: 一.方法区(METHOD AREA): 1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型 ...

  6. redis 使用详解

    前戏: 又到了最喜欢的前戏部分,这个前戏可能有点长: Nosql和sql的区别 存储结构与mysql这一种关系型数据库完全不同,nosql存储的是KV形式 应用场景不同,sql支持关系复杂的数据查询, ...

  7. 集合流之"交集(相同)和差集(区别的)"的使用

    一.需求 今天做的是将两个字符串转为数组后再转集合,然后利用集合的流stream来进行差集过滤 二.差集代码 差集:将两个集合相同的数据去掉,留下不同的数据 1 @Test 2 public void ...

  8. ctfhub web信息泄露备份文件下载(网站源码,back文件)

    网站源码 进入环境,首先我们用bp抓一下包 在HTTP请求方式GET/后添加两个负载,一个用于爆破文件名,一个用于爆破后缀名 得知网页源码的备份形式为www.zip,下载网页源码 打开记事本文件 发现 ...

  9. 什么是CSS Modules ?我们为什么需要他们

    原文地址:https://css-tricks.com/css-mo...最近我对CSS Modules比较好奇.如果你曾经听说过他们,那么这篇博客正适合你.我们将去探索它的目的和主旨.如果你同样很好 ...

  10. iView 一周年了,同时发布了 2.0 正式版,但这只是开始...

    两年前,我开始接触 Vue.js 框架,当时就被它的轻量.组件化和友好的 API 所吸引.之后我将 Vue.js 和 Webpack 技术栈引入我的公司(TalkingData)可视化团队,并经过一年 ...