内存模型

Go内存模型指定了在何种条件下可以保证在一个 goroutine 中读取变量时观察到在不同 goroutine 中写入相同变量所产生的值,修改多个 goroutine 同时访问的数据的程序必须序列化这种访问,要序列化访问,请使用通道操作或其他同步原语(例如 sync 和 sync/atomic 包中的原语)保护数据。

广义的 Happens Before

到底如何理解(Happens Before)先行发生,我也思考为啥需要确保先行发生这个顺序呢,直到看到了《Time, Clocks, and the Ordering of Events in a Distributed System》,分布式系统内的事件排序,涉及到最深层的本质问题。图灵奖得主Lamport在1978年发表的经典论文。正是对这些本质问题的一个系统化的阐述

论文主要是讲三个基础概念:时间(Time)、时钟(Clock)、事件排序(Ordering of Events)。它们之间的关系大概如下

时间

时间只是一个物理学上的概念。到底是什么,没几个人真正理解,我们是无法感受时间的,也无法测量时间,我们感知的时间其实是事件的流逝,比如看到太阳升起我们知道是早晨,而落日则意味着傍晚。

时钟

时钟分两种,一种是物理时钟,或者叫实时时钟;另一种是逻辑时钟。物理时钟是对时间的一种度量;现实中的物理时钟肯定是有误差的。而逻辑时钟是跟物理时间无关的,用于对每一个发生的事件指派一个单调递增的数值,是系统执行节拍的一种内部表示。

事件排序

两个不同的事件,可能具有先后关系,它们之间是能够排序的;也可能两个事件之间根本无法按照先后关系来排序。也就是说,事件排序是偏序的(Partial Ordering)

如果我们说事件a在事件b之前发生,直觉上的含义大概是:事件a发生的时间比事件b发生的时间要早。然而,这种判定事件之间次序的方式,是依赖物理时间的。这要求我们必须引入物理时钟才行,而物理时钟不可能百分之百精确。

Lamport在定义事件之间的关系的时候特意避开了物理时间。这就是著名的「Happened Before」关系(用符号“→”来表示)。见下面进程P、Q和R的消息时空图(注意图中自下而上时间递增)

结合上图我们举例解释一下「Happened Before」关系:

  • 同一进程内部先后发生的两个事件之间,具有「Happened Before」关系。比如,在进程Q内部,q2表示一个消息接收事件,q4表示另一个消息的发送事件,q2排在q4前面执行,所以q2→q4。
  • 同一个消息的发送事件和接收事件,具有「Happened Before」关系。比如,p1和q2分别表示同一个消息的发送事件和接收事件,所以p1→q2;同理,q4→r3。
  • 「Happened Before」满足传递关系。比如,由p1→q2,q2→q4和q4→r3,可以推出p1→r3。

这种「Happened Before」关系的关键在于,它是一种偏序关系。也就是说,并不是所有事件之间都具有「Happened Before」关系。比如p1和q1两个事件就是无法比较的,q4和r2也是无法比较的。

To specify the requirements of reads and writes, we definehappens before, a partial order on the execution of memory operations in a Go program 为了指定读写的要求,我们定义了happens before,一个Go程序中内存操作执行的偏序 官方文档

我的理解

可以把 「Happened Before」 理解成因果关系的另一种方式,相当于是说,a→b意味着事件a有可能在因果性上对事件b产生影响。如果两个事件谁也无法影响对方,那么它们就属于并发关系

Memory Reordering

a = 1;b = 2;ba

下面这段代码会有怎样的输出

显而易见的几种结果

令人意外的结果


内存重排的目的

  • 减少读写等待导致的性能降低
  • 最大化提高 CPU 利用率

CPU 架构

如下图,现代CPU为了“抚平”内核、内存、硬盘之间的速度差异,搞出了各种策略,例如三级缓存等。为了让(2)不必等待(1)的执行“效果”可见之后才能执行,我们可以把(1)的效果保存到store buffer

如下,先执行(1)和(3),将他们直接写入storebuffer,接着执行(2)和(4)。“奇迹”要发生了:(2)看了下store buffer,并没有发现有B的值,于是从Memory读出了0,(4)同样从Memory读出了0。最后,打印出了00。

因此,对于多线程的程序,所有的CPU都会提供“锁”支持,称之为barrier,或者fence。它要求:barrier指令要求所有对内存的操作都必须要“扩散”到memory之后才能继续执行其他对memory的操作。因此,我们可以用高级点的atomiccompare-and-swap,或者直接用更高级的锁,通常是标准库提供。

Go 的 Happens Before

单一goroutine

单一goroutine中Happens Before所要表达的顺序就是程序执行的顺序

多goroutine

正式由于多goroutine是没有办法确定 Happens Before因果关系的,因为两个都是独立存在的。 我们不能够确定事件排序,这个时候就需要 Synchronization(同步) Synchronization

  • 传统的线程模型(通常在编写Java、C++和Python程序时使用)程序员在线程之间通信需要使用共享内存。通常,共享数据结构由锁保护,线程将争用这些锁来访问数据。
  • CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 我理解的channel 的本质就是确立了 Happened Before关系 a->b b->c 就可以确立因果关系,进而解决两个事件谁也无法影响对方,也就是解决并发关系