内存模型是非常重要的,理解Go的内存模型会就可以明白很多奇怪的竞态条件问题,"The Go Memory Model"的原文在这里,读个四五遍也不算多。

这里并不是要翻译这篇文章,英文原文是精确的,但读起来却很晦涩,尤其是happens-before的概念本身就是不好理解的,很容易跟时序问题混淆。大多数读者第一遍读Go的内存模型时基本上看不懂它在说什么。所以我要做的事情用不怎么精确但相对通俗的语言解释一下。

先用一句话总结,Go的内存模型描述的是"在一个groutine中对变量进行读操作能够侦测到在其他goroutine中对该变量的写操作"的条件。

内存模型相关bug一例

为了证明这个重要性,先看一个例子。下面一小段代码:

package main

import (
  "sync"
  "time"
)

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++
      time.Sleep(time.Millisecond)
      count--
      <-ch
      wg.Done()
    }()
  }
  wg.Wait()
}

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

go run -race test.go

编译器可以检测到16和18行是存在竞态条件的,也就是count并没像我们想要的那样在临界区执行。继续往下看,读完这一节,回头再来看就可以明白为什么这里有bug了。

happens-before

happens-before是一个术语,并不仅仅是Go语言才有的。简单的说,通常的定义如下:

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

无论使用哪种编程语言,有一点是相同的:如果操作A和B在相同的线程中执行,并且A操作的声明在B之前,那么A happens-before B。

int A, B;
void foo()
{
  // This store to A ...
  A = 5;
  // ... effectively becomes visible before the following loads. Duh!
  B = A * A;
}

还有一点是,在每门语言中,无论你使用那种方式获得,happens-before关系都是可传递的:如果A happens-before B,同时B happens-before C,那么A happens-before C。当这些关系发生在不同的线程中,传递性将变得非常有用。

刚接触这个术语的人总是容易误解,这里必须澄清的是,happens-before并不是指时序关系,并不是说A happens-before B就表示操作A在操作B之前发生。它就是一个术语,就像光年不是时间单位一样。具体地说:

  1. A happens-before B并不意味着A在B之前发生。
  2. A在B之前发生并不意味着A happens-before B。

这两个陈述看似矛盾,其实并不是。如果你觉得很困惑,可以多读几篇它的定义。后面我会试着解释这点。记住,happens-before 是一系列语言规范中定义的操作间的关系。它和时间的概念独立。这和我们通常说”A在B之前发生”时表达的真实世界中事件的时间顺序不同。

A happens-before B并不意味着A在B之前发生