The Go Memory Model

memory model 其实各大语言似乎都有,所以看到下面的代码你大概不会陌生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a string
var done bool

func setup() {
a = "hello, world"
done = true
}

func main() {
go setup()
for !done {
}
print(a)
}

相信你可以用经验判断上面的例子哪里有问题,实际上,一个程序面临着:

  • 编译器可能把无关的代码乱序以提高效率
  • CPU 可能会产生执行上的乱序(比如 Intel 的 Store-Load 乱序)
  • 多核内存上有各种各样的麻烦
    • 虽说内存和并发关系暧昧不清,但是毫无疑问,前者对写代码是存在影响的。有的时候甚至会给人带来奇怪的印象和吊诡的设计,比如 C 和 Java 不同含义的 volatile.

那么 Go 同样要面临这样的问题,下面的代码能够很好的运作。

1
2
3
4
5
6
7
8
9
10
11
var wg sync.WaitGroup
xxx := nil
wg.Add(1)
go func() {
defer wg.Done()
// logic..
xxx = val
}

wg.Done()
// read xxx

还有:

1
2
3
4
5
6
7
xChan = make(chan X)
go func() {
xChan <- val
}

xxx := <- xChan
// read xxx

为什么呢?我们来看看这个 blog 吧:https://golang.org/ref/mem

Happens Before

Within a single goroutine, the happens-before order is the order expressed by the program.

A read r of a variable v is allowed to observe a write w to v if both of the following hold:

  1. r does not happen before w.
  2. There is no other write w’ to v that happens after w but before r.

To guarantee that a read r of a variable v observes a particular write w to v, ensure that w is the only write r is allowed to observe. That is, r is guaranteed to observe w if both of the following hold:

  1. w happens before r.
  2. Any other write to the shared variable v either happens before w or after r.

This pair of conditions is stronger than the first pair; it requires that there are no other writes happening concurrently with w or r.

原文还是比较精炼的,我就直接翻译了。目前的 happens before 针对的对象是单个变量。注意

Reads and writes of values larger than a single machine word behave as multiple machine-word-sized operations in an unspecified order.

关于 happens before, Go 语言的介绍还是很简单的,我倾向于阅读一下 C++ Concurrency In Action, 摘录下面关于 happens-before 的一段

At the basic level, inter-thread happens-before is relatively simple and relies on the synchronizes-with relationship introduced in section 5.3.1: if operation A in one thread synchronizes-with operation B in another thread, then A inter-thread happens- before B. It’s also a transitive relation: if A inter-thread happens-before B and B inter- thread happens-before C, then A inter-thread happens-before C. You saw this in listing 5.2 as well.

Inter-thread happens-before also combines with the sequenced-before relation: if operation A is sequenced before operation B, and operation B inter-thread happens- before operation C, then A inter-thread happens-before C. Similarly, if A synchronizes- with B and B is sequenced before C, then A inter-thread happens-before C. These two together mean that if you make a series of changes to data in a single thread, you need only one synchronizes-with relationship for the data to be visible to subsequent opera- tions on the thread that executed C.

以上你就有了关于 happens before 的基本认知。

黑暗面

下面内容是我胡扯的

This hardware must obey the following ordering constraints [McK05a, McK05b]:

  1. Each CPU will always perceive its own memory accesses as occurring in program order.
  2. CPUs will reorder a given operation with a store only if the two operations are referencing different locations.
  3. All of a given CPU’s loads preceding a read memory barrier (smp_rmb()) will be perceived by all CPUs to precede any loads following that read memory barrier.
  4. All of a given CPU’s stores preceding a write mem- ory barrier (smp_wmb()) will be perceived by all CPUs to precede any stores following that write memory barrier.
  5. All of a given CPU’s accesses (loads and stores) preceding a full memory barrier (smp_mb()) will be perceived by all CPUs to precede any accesses following that memory barrier.

Synchronization

Initialization

Program initialization runs in a single goroutine, but that goroutine may create other goroutines, which run concurrently.

If a package p imports package q, the completion of q‘s init functions happens before the start of any of p‘s.

The start of the function main.main happens after all init functions have finished.

就是 init 阶段形成一棵树或者DAG,后被 import 的先被初始化。

Goroutine creation

The go statement that starts a new goroutine happens before the goroutine’s execution begins.

官方文档的例子很好,一字不易

1
2
3
4
5
6
7
8
9
10
var a string

func f() {
print(a)
}

func hello() {
a = "hello, world"
go f()
}
  1. go before goroutine’s begin
  2. a 会先被初始化

Goroutine destruction

The exit of a goroutine is not guaranteed to happen before any event in the program. For example, in this program:

1
2
3
4
5
6
var a string

func hello() {
go func() { a = "hello" }()
print(a)
}

the assignment to a is not followed by any synchronization event, so it is not guaranteed to be observed by any other goroutine. In fact, an aggressive compiler might delete the entire go statement.

If the effects of a goroutine must be observed by another goroutine, use a synchronization mechanism such as a lock or channel communication to establish a relative ordering.

(其实我很好奇,这一段能不能过编译)

Channel communication

A send on a channel happens before the corresponding receive from that channel completes.

A receive from an unbuffered channel happens before the send on that channel completes.

回想起开头的代码,不难理解为什么下列代码是合理的

1
2
3
4
5
6
7
xChan = make(chan X)
go func() {
xChan <- val
}

xxx := <- xChan
// read xxx

代码还有个比较有意思的地方是限流

1
2
3
4
5
6
7
8
9
10
11
12
var limit = make(chan int, 3)

func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}

sync 库

A single call of f() from once.Do(f) happens (returns) before any call of once.Do(f) returns.

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n**+1 to l.Lock.

如果 Lock Once 有问题,那我们的程序真应该出毛病了!实际上,程序最早的 wg.Done 也有 happens-before 语义!