[TOC]
语言的内存模型定义了并行状态下拥有确定读取和写入的时序的条件。 Go 的 goroutine 采取并发的形式运行在多个并行的线程上, 而其内存模型就明确了 对于一个 goroutine 而言,一个变量被写入后一定能够被读取到的条件。 在 Go 的内存模型中有事件时序的概念,并定义了 happens before ,即表示了在 Go 程序中执行内存操作的一个偏序关系。
我们不妨用 < 表示 happens before,则如果事件 e1 < e2,则 e2 > e1。 同样,如果 e1 ≥ e2 且 _e1 ≤ e2,则 e1 与 e2 happen concurrently (e1 = e2)。 在单个 goroutine 中,happens-before 顺序即程序定义的顺序。
我们稍微学院派的描述一下偏序的概念。 (严格)偏序在数学上是一个二元关系,它满足自反、反对称和传递性。happens before(<)被称之为偏序,如果满足这三个性质:
- (反自反性)对于 ∀_e1_∈{事件},有:非 e1 < e1;
- (非对称性)对于∀_e1_, _e2_∈{事件},如果 e1 ≤ e2,e2 ≤ e1 则 e1 = e2,也称 happens concurrently;
- (传递性)对于∀_e1_, e2, e3 ∈{事件},如果 e1 < e2,e2 < e3,则 e1 < e3。
可能我们会认为这种事件的发生时序的偏序关系仅仅只是在探讨并发模型,跟内存无关。 但实际上,它们既然被称之为内存模型,就是因为它们与内存有着密切关系。 并发操作时间偏序的条件,本质上来说,是定义了内存操作的可见性。
编译器和 CPU 通常会产生各种优化来影响程序原本定义的执行顺序,这包括:编译器的指令重排、 CPU 的乱序执行。 除此之外,由于缓存的关系,多核 CPU 下,一个 CPU 核心的写结果仅发生在该核心最近的缓存下, 要想被另一个 CPU 读到则必须等待内存被置换回低级缓存再置换到另一个核心后才能被读到。
Go 中的 happens before 有以下保证:
- 初始化:
main.init
<main.main
- goroutine 创建:
go
<goroutine 开始执行
- goroutine 销毁:
goroutine 退出
= ∀e
- channel: 如果 ch 是一个 buffered channel,则
ch<-val
<val <- ch
- channel: 如果 ch 是一个 buffered channel 则
close(ch)
<val <- ch & val == isZero(val)
- channel: 如果 ch 是一个 unbuffered channel 则,
ch<-val
>val <- ch
- channel: 如果
len(ch) == C
,则从 channel 中收到第 k 个值
<k+C 个值得发送完成
- mutex: 如果对于 sync.Mutex/sync.RWMutex 的锁 l 有 n < m, 则第 n 次调用
l.Unlock()
< 第 m 次调用 l.Lock() 的返回 - mutex: 任何发生在 sync.RWMutex 上的调用
l.RLock
, 存在一个 n 使得l.RLock
> 第 n 次调用l.Unlock
,且与之匹配的l.RUnlock
< 第 n+1 次调用 l.Lock - once: f() 在 once.Do(f) 中的调用 < once.Do(f) 的返回.
Go under the hood | CC-BY-NC-ND 4.0 & MIT © changkun