The Go Memory Model

Go 的内存模型指定一系列条件,保证「在一个 goroutine 中读取变量可以观察到其他 goroutine 中对该变量所写的值」即安全的在不同的协程中读写变量

🌰

func main() {
	var wg sync.WaitGroup
	var count int
	var ch = make(chan bool, 1)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			ch <- true // 并不会被阻塞,存在竞争
			count++ // 1
			time.Sleep(time.Millisecond)
			count-- // 2
			<-ch
			wg.Done()
		}()
	}
	wg.Wait()
}

这里把buffered channel作为 semaphore 来使用,表面上看最多允许一个goroutine对count进行++和--,但其实这里是有bug的。根据Go语言的内存模型,对count变量的访问并没有形成临界区。编译时开启竞态检测可以看到这段代码有问题:

go run -race test.go

编译器可以检测到 1, 2 两处存在竞态条件,也就是count并没像我们想要的那样在临界区执行。

happens-before 偏序 ≤

假设 A 和 B 表示一个多线程的程序执行的两个操作。如果 A happens-before B,那么 A 操作对内存的影响 将对执行 B 的线程(且执行 B 之前)可见。

int A, B;
void foo()
{
  // This store to A ...
  A = 5;
  // ... effectively becomes visible before the following loads. Duh!
  B = A * A;
}
  1. A happens-before B并不意味着A在B之前发生
  2. A 在 B 之前发生并不意味着 A happens-before B

happens-before 是一系列语言规范中定义的操作间的关系,它和时间的概念独立

Channel

关于channel的happens-before在Go的内存模型中提到了三种情况:

  1. 对一个channel的发送操作 happens-before 相应channel的接收操作完成
  2. 关闭一个channel happens-before 从该Channel接收到最后的返回值0
  3. 不带缓冲的channel的接收操作 happens-before 相应channel的发送操作完成