Go语言进阶(三)内存模型

Memory Model(内存模型)

Go内存模型官方ref:https://go.dev/ref/mem

如何保证在一个 goroutine 中看到在另一个 goroutine 修改的变量的值,如果程序中修改数据时有其他 goroutine 同时读取,那么必须将读取串行化。为了串行化访问,请使用 channel 或其他同步原语,例如 syncsync/atomic 来保护数据。

Happen-Before

在一个 goroutine 中,读和写一定是按照程序中的顺序执行的。即编译器和处理器只有在不会改变这个 goroutine 的行为时才可能修改读和写的执行顺序。由于重排,不同的 goroutine 可能会看到不同的执行顺序。例如,一个goroutine 执行 a = 1;b = 2;,另一个 goroutine 可能看到 ba 之前更新。

关于Happen-Before可以参考:happens-before是什么?JMM最最核心的概念,看完你就懂了 – 程序员七哥的文章 – 知乎

image-20211208160016241

Memory Reordering(内存重排)

用户写下的代码,先要编译成汇编代码,也就是各种指令,包括读写内存的指令。CPU 的设计者们,为了榨干 CPU 的性能,无所不用其极,各种手段都用上了,你可能听过不少,像流水线、分支预测等等。其中,为了提高读写内存的效率,会对读写指令进行重新排列,这就是所谓的内存重排,英文为 Memory Reordering

举个栗子:

串行环境下,上面这个重排完全没有问题。但是在多核心场景下,没有办法轻易地判断两段程序是“等价”的。如下:

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

image-20211208160712324

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

image-20211208161105672

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

再回到Memory Model

为了说明读和写的必要条件,我们定义了先行发生(Happens Before)。

  • 如果事件 e1 发生在 e2 前,我们可以说 e2 发生在 e1 后。
  • 如果 e1不发生在 e2 前也不发生在 e2 后,我们就说 e1 和 e2 是并发的。

在单一的独立的 goroutine 中先行发生的顺序即是程序中表达的顺序。

当下面条件满足时,对变量 v 的读操作 r 是被允许看到对 v 的写操作 w 的:

  • r 不先行发生于 w
  • 在 w 后 r 前没有对 v 的其他写操作

为了保证对变量 v 的读操作 r 看到对 v 的写操作 w,要确保 w 是 r 允许看到的唯一写操作。即当下面条件满足时,r 被保证看到 w:

  • w 先行发生于 r
  • 其他对共享变量 v 的写操作要么在 w 前,要么在 r 后。

这一对条件比前面的条件更严格,需要没有其他写操作与 w 或 r 并发发生。

两种情况:

  1. 单个 goroutine 中没有并发,所以上面两个定义是相同的:读操作 r 看到最近一次的写操作 w 写入 v 的值。

  2. 当多个 goroutine 访问共享变量 v 时,它们必须使用同步事件来建立先行发生这一条件来保证读操作能看到需要的写操作。

  • 对变量 v 的零值初始化在内存模型中表现的与写操作相同。

  • 对大于 single machine word 的变量的读写操作表现的像以不确定顺序对多个 single machine word 的变量的操作

更多参考:https://www.jianshu.com/p/5e44168f47a3