Go的内存模型

这篇文章主要是为了阅读一篇 go 的文档,解释了什么状况下一个 goroutine 对变量写的值能被另外一个 goroutine 可靠观察到,主要以意译为主,括号内为我的理解。html

不管是用单个通道来守护并发数据的实现仍是使用 sync 和 sync/atomic 中的同步原语的实现,程序中多个 goroutine 并发操做相同数据时必定是串行的。(两种常见的并发模型: 使用专门 channel 来操做并发访问的数据,其它 goroutine 把本身的操做请求发给这个 channel;多个 goroutine 抢锁来操做数据)golang

在单个 goroutine 中,对一个变量读写操做的真正执行顺序必需要和代码中的顺序具备相同的执行效果,就是说,编译器和处理器可能会对单个 goroutine 内的一些读写操做进行从新排序,但调整顺序先后的执行结果要跟按代码中的顺序执行结果一致。但这种从新排序不会考虑多个 goroutine 的状况,一个 goroutine 中的代码所展现的执行顺序和其它 goroutine 实际观察 ( 好比另外一个 goroutine 监听这个 goroutine 中某些变量的变化 ) 到的这个 goroutine 的执行顺序可能会不一样。好比一个 goroutine 代码中是依次执行 a=1 和 b=2,另外一个 goroutine 可能会观察到 b 先被赋值为 2,而后再是 a=1。并发

Happens Before app

为了说清楚读和写的请求,先定义一下这个 happens before : 用来表达 Go 程序在内存中的一小段执行顺序,当咱们说 e1 在 e2 以前发生时, 就是在说 e2 在 e1 以后发生。当 e1 没有发生在 e2 以前,并且 e2 没有发生在 e1 以后时,咱们说 e1 和 e2 这时是并发的 ( 即咱们没法可靠判断 e1 和 e2 的执行顺序 ) 。函数

在单个goroutine内, happens before 的顺序是由代码表达的顺序决定的this

对于变量 v 的一个读 r ,若是不可靠观察到( 相对于可靠观察到 ) 写 w 对 v 的操做,那么 r 和 w 要知足:atom

  1. r 不能发生在 w 以前(即 r,w 要么并发发生,要么 r 在 w 以后发生)
  2. 在 w 以后且在 r 以前没有其余的对 v 的写 ( 即其它的写要么与 w 并发发生,要么与 r 并发发生,要么发生在 w 以前,要么发生在 r 以后 )

(能够看到,上面约束中并发就是不可靠的来源)而若是为了保证对 v 的读 r 可以观察到对 v 特定的一次写 w ,就是说要 r 仅观察到这特定的一次 w , 为了实现 r 可以可靠观察到此次 w ,那要知足:spa

  1. w 发生在 r 以前 ( 相比前文两条约束排除了 r,w 并发发生 )
  2. 其它任何对共享变量 v 的写,要么发生在 w 以前,要么发生在 r 以后 ( 一样相比以前排除了其它的写与 w 并发发生,以及其它写与r并发发生的两种状况 )

上面后两条约束要强于前两对,后者要求在 w 和 r 发生时没有其它的 w 并发发生。在单个 goroutine 内是不可能并发的,因此单个 goroutine 的状况下上面两对约束是一个意思:对 v 的读可以观察到最近一次的 w。可是在多个 goroutine 共享 v 的状况下, 就必须使用同步原语创建可靠的 happens-before 来保证一次读可以读到指定的一次写。code

使用 v 类型的零值对v进行初始化和一次对 v 的写操做,在内存模型中是同样的htm

对于一个超过一个字( 即机器字 )的值来讲,对它的读写操做至关于多个不肯定顺序的单字操做

同步中的 happens before:

几种可靠的 happens before 发生顺序

1, 若是 p 导入 q 包, 那么 q 的 init 函数是可靠发生在 p 中任何逻辑以前的
2, 而 main 包中 main 函数是可靠发生在全部 init 函数完成以后
3, goroutine 建立时的 go 声明可靠发生在这个 goroutine 开始执行以前

var a string
func f() {
  print(a)
}
func hello() {
  a = "hello, world"   // a是被先赋值
  go f()               // go f()后执行, 因此print(a)必定会打印"hello,world"
}

4, 不使用同步机制的话, 没法可靠保证 goroutine 的退出发生时机

var a string
func hello() {
  go func() { a = "hello" }()
  print(a)  // 这能够打印空字符串, 也能够打印hello, 甚至一些激进的编译器直接删除建立goroutine的那句
}

( 到这个位置介绍的都还比较符合直觉, 可是接下来四条结论就比较违反直觉, 至少是个人直觉, 先看结论, 后面我再结合代码给出理解 )

5, 一次发送可靠发生在对应此次发送的接收完成以前
6, 通道关闭可靠发生在接收方收到通道类型的零值以前
7, 对于无缓冲通道的接收是可靠发生在发送完成以前
8, 第 k 次对容量为 C 的缓冲通道的接收是可靠发生在第 k+C 次的发送完成以前

( 注: 这个位置须要对比5, 6, 7, 8的原文理解一下,
文档原文以下:
5, A send on a channel happens before the corresponding receive from that channel completes.
6, The closing of a channel happens before a receive that returns a zero value because the channel is closed.
7, A receive from an unbuffered channel happens before the send on that channel completes.
8, The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.

理解这些要把发送分解为两个过程:发送,发送完成,一样接收也是:接收,接收完成。前两句就是一个意思, 由于第二句中通道关闭就是发送通道类型的零值, 因此前两句就是说发送行为是发生在接收完成以前, 即若是接收完成了,那么发送必定发生了,但发送是否完成还不必定,强调的是接收完成的时候哪些是可靠发生了的。

var c = make(chan int, 10)
var a string
func f() {
  a = "hello"   // 写 a 发生在发送 0 给 c 以前
  c <- 0        // 发送 0 给 c 发生在 c 接收完成以前
}
func main() {
  go f()
  <-c       // c 接收完成发生在打印 a 以前
  print(a)  // 可靠打印 hello 
}

后面7,8两句对于非缓冲通道和缓冲通道满了状况的描述比较使人费解, 我在另一篇介绍通道的文档中找到这一段

If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.

若是是无缓冲通道,发送者会一直阻塞到接收者接收完成 ( has received ) 这个值。若是是缓冲通道,发送者会一直阻塞直到值被复制到缓冲区,若是缓冲区满了,那发送者阻塞到接收者从缓冲区中取走一个值。

这段介绍和 7,8 的结论是一致的,即对于阻塞状态下的通道,不管是无缓冲通道仍是缓冲通道满了,接收完成必定是先于发送完成的,这里一直使用的是 has received 和 has retrieved, 对应 7,8 中的 completes. 因此对于无缓冲通道或者缓冲通道满了的状况,发送和接收最终完成,必定是接收先完成,而后发送才完成

var c = make(chan int)
var a string
func f() {
  a = "hello, world"
  <-c       // 这行先于 c<-0 完成以前完成, a 可靠赋值
}
func main() {
  go f()
  c <- 0
  // c的发送发生在print以前,
  // 而 c 的接收发生在 c 发送完成以前完成
  // 而 a 在 c 的接收以前完成赋值
  // 因此a赋值, 到c接收完成, 再到 main 中 c 发送完成, 最后可靠打印 a
  print(a)
}

另外,这段话还提供了缓冲通道的细节: 发送者等待的是把值复制到缓冲区,而不是接收者完成, 而接收者等待的是缓冲区的值, 因此对于缓冲未满的状况, 发送者要先完成把值复制到缓冲区,接收者才能从缓冲区读到值,就是 5 的结论,而非缓冲通道发送者等待的是接收者完成。

这细节..., 多是为了知道从阻塞状态下通道解阻塞后,接收者先走一步吧

最后用图来综上所述吧
image

ok, 接着读这篇内存模型的文档 )

经过第 8 条结论, 能够用缓冲通道来模拟计数型的同步机制:通道缓冲容量表明最大容许活跃的原语数量,达到数量以后, 若是还想使用同步原语就要等待其它活跃的同步原语被释放,经常使用来限制并发,上代码:

var limit = make(chan int, 3)
func main() {
  for _, w := range work {  // 虽然for为每一个work建立了一个goroutine, 但这些goroutine并非同时活跃的   
    go func(w func()) {  
      limit <- 1            // limit满了状况下, goroutine就会阻塞在这里
      w()
      <- limit              // 直到其它goroutine执行完w(), 从limit中取一个值出来, 任什么时候候最大活跃 worker 只有3个
    }(w)
  }
}

锁中的happens before:

9, 对于 Mutex 或者 RWMutex 类型 l, 和整型 n, m, 其中n<m, n次对 l.Unlock( ) 可靠发生在 m 次的 l.Lock( ) 以前

var l sync.Mutex
var a string
func f() {
    a = "hello"
    l.Unlock()  // n = 1
}
func main() {
    l.lock()  // m = 1 
    go f()
    l.lock()  // m = 2 上面n=1可靠发生在m=2以前, 因此可靠打印hello
    print(a)
}

10, 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.
这句意思是下图
image

Once中的 happens before:

11, Once 提供了并发场景下的初始化方案, 多个 goroutine 调用 once.Do(f), 仅会有一个真正执行了 f( ), 其它的 goroutine 会阻塞等待执行的那个返回, 即其中一个真正执行的那个 goroutine 执行 f( ) 会发生在任何一 once.Do(f)返回以前

var a string
var once sync.Once
func setup() {
    a = "hello"
}
func doprint() {
    once.Do(setup)
    print(a)
}
func twoprint() {
    go doprint()    // 这两个goroutine中仅有一个真正执行了setup(),可是两个都会阻塞到setup()被执行完成
    go doprint()    // 因此a写入发生在once.Do(setup)以前,print(a)会可靠打印两遍hello
}

不正确的同步:

var a, b int
func f() {
    a = 1
    b = 2
}
func g() {
    print(b)
    print(a)
}
func main() {
    go f()
    g()    // 这个位置几乎可print任何组合, 0-0, 0-1, 2-0, 1-2, 由于f的goroutine和主goroutine没有任何同步,
}
var a string
var done bool

func setup() {
    a = "hello"
    done = true
}
func doprint() {
    if !done {          // 重点是, 这个逻辑是在暗示读到了done就能读到在done以前写的a, 其实是,在没有同步机制下, 读到了done也不必定
        once.Do(setup)  // 能读到a      
    }
    print(a)
}
func twoprint() {
    go doprint()   // 可能两个goroutine都会阻塞在once.Do(setup)位置, 其中一个真正执行了setup, 而另外一个不会执行, 这个为执行的goroutine
    go doprint()   // 就没法可靠观察到那个执行setup的goroutine对a的写, 因此会有一个空字符串
}
var a string
var done bool

func setup() {
    a = "hello"
    done = true
}
func main() {
    go setup()
    for !done {}   // 这个也是在暗示读到done就能读到a,一样这个done可能被main goroutine读到, 但不必定表示就能读到a, 还有就是这个done
    print(a)       // 也有可能永远不会被main读到,
}
type T struct {
    msg string
}
var g *T
func setup() {
    t := new(T)
    t.msg = "hello"
    g = t
}
func main() {
    go setup()
    for g == nil {}    //  main gorotine和setup gorotine共享了g, 因此main能够观察到g, 可是对g.msg的写没法可靠保证。
    print(g.msg)
}

只要显式使用同步原语就能够解决上面的问题

相关文章
相关标签/搜索