Go的内存模型

转载请注明出处,原文连接http://tailnode.tk/2017/01/Go...node

说明

翻译自The Go Memory Modelgolang

介绍

如何保证在一个goroutine中看到在另外一个goroutine修改的变量的值,这篇文章进行了详细说明。安全

建议

若是程序中修改数据时有其余goroutine同时读取,那么必须将读取串行化。为了串行化访问,请使用channel或其余同步原语,例如syncsync/atomic来保护数据。并发

先行发生

在一个gouroutine中,读和写必定是按照程序中的顺序执行的。即编译器和处理器只有在不会改变这个goroutine的行为时才可能修改读和写的执行顺序。因为重排,不一样的goroutine可能会看到不一样的执行顺序。例如,一个goroutine执行a = 1;b = 2;,另外一个goroutine可能看到ba以前更新。
为了说明读和写的必要条件,咱们定义了先行发生(Happens Before)--Go程序中执行内存操做的偏序。若是事件e1发生在e2前,咱们能够说e2发生在e1后。若是e1不发生在e2前也不发生在e2后,咱们就说e1e2是并发的。
在单独的goroutine中先行发生的顺序便是程序中表达的顺序。
当下面条件知足时,对变量v的读操做r被容许看到对v的写操做w的:app

  1. r不先行发生于w函数

  2. wr前没有对v的其余写操做atom

为了保证对变量v的读操做r看到对v的写操做w,要确保wr容许看到的惟一写操做。即当下面条件知足时,r 被保证看到w线程

  1. w先行发生于r翻译

  2. 其余对共享变量v的写操做要么在w前,要么在r后。
    这一对条件比前面的条件更严格,须要没有其余写操做与wr并发发生。code

单独的goroutine中没有并发,因此上面两个定义是相同的:读操做r看到最近一次的写操做w写入v的值。当多个goroutine访问共享变量v时,它们必须使用同步事件来创建先行发生这一条件来保证读操做能看到须要的写操做。
对变量v的零值初始化在内存模型中表现的与写操做相同。
对大于一个字的变量的读写操做表现的像以不肯定顺序对多个一字大小的变量的操做。

同步

初始化

程序的初始化在单独的goroutine中进行,但这个goroutine可能会建立出并发执行的其余goroutine。
若是包p引入(import)包q,那么q的init函数的结束先行发生于p的全部init函数开始
main.main函数的开始发生在全部init函数结束以后

建立goroutine

go关键字开启新的goroutine,先行发生于这个goroutine开始执行,例以下面程序:

var a string

func f() {
    print(a)
}

func hello() {
    a = "hello, world"
    go f()
}

调用hello会在以后的某时刻打印出"hello, world"(可能在hello返回以后)

销毁goroutine

gouroutine的退出并不会保证先行发生于程序的任何事件。例以下面程序:

var a string

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

没有用任何同步操做限制对a的赋值,因此并不能保证其余goroutine能看到a的变化。实际上,一个激进的编译器可能会删掉整个go语句。
若是想要在一个goroutine中看到另外一个goroutine的执行效果,请使用锁或者channel这种同步机制来创建程序执行的相对顺序。

channel通讯

channel通讯是goroutine同步的主要方法。每个在特定channel的发送操做都会匹配到一般在另外一个goroutine执行的接收操做。
在channel的发送操做先行发生于对应的接收操做完成
例如:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

这个程序能保证打印出"hello, world"。对a的写先行发生于在c上的发送,先行发生于在c上的对应的接收完成,先行发生于print
对channel的关闭先行发生于接收到零值,由于channel已经被关闭了。
在上面的例子中,将c <- 0替换为close(c)还会产生一样的结果。
无缓冲channel的接收先行发生于发送完成
以下程序(和上面相似,只交换了对channel的读写位置并使用了非缓冲channel):

var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

此程序也能保证打印出"hello, world"。对a的写先行发生于从c接收,先行发生于向c发送完成,先行发生于print

若是是带缓冲的channel(例如c = make(chan int, 1)),程序不保证打印出"hello, world"(可能打印空字符,程序崩溃或其余行为)。
在容量为C的channel上的第k个接收先行发生于从这个channel上的第k+C次发送完成。
这条规则将前面的规则推广到了带缓冲的channel上。能够经过带缓冲的channel来实现计数信号量:channel中的元素数量对应着活动的数量,channel的容量表示同时活动的最大数量,发送元素获取信号量,接收元素释放信号量,这是限制并发的一般用法。
下面程序为work中的每一项开启一个goroutine,但这些goroutine经过有限制的channel来确保最多同时执行三个工做函数(w)。

var limit = make(chan int, 3)

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

sync包实现了两个锁的数据类型sync.Mutex和sync.RWMutex。
对任意的sync.Mutex或sync.RWMutex变量l和n < m,n次调用l.Unlock()先行发生于m次l.Lock()返回
下面程序:

var l sync.Mutex
var a string

func f() {
    a = "hello, world"
    l.Unlock()
}

func main() {
    l.Lock()
    go f()
    l.Lock()
    print(a)
}

能保证打印出"hello, world"。第一次调用l.Unlock()(在f()中)先行发生于main中的第二次l.Lock()返回, 先行发生于print。
对于sync.RWMutex变量l,任意的函数调用l.RLock知足第n次l.RLock后发生于第n次调用l.Unlock,对应的l.RUnlock先行发生于第n+1次调用l.Lock。

Once

sync包的Once为多个goroutine提供了安全的初始化机制。能在多个线程中执行once.Do(f),但只有一个f()会执行,其余调用会一直阻塞直到f()返回。
经过once.Do(f)执行f()先行发生(指f()返回)于其余的once.Do(f)返回。
以下程序:

var a string
var once sync.Once

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

func doprint() {
    once.Do(setup)
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

调用twoprint会打印"hello, world"两次。setup只在第一次doprint时执行。

错误的同步方法

注意,读操做r可能会看到并发的写操做w。即便这样也不能代表r以后的读能看到w以前的写。
以下程序:

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

g可能先打印出2而后是0。
这个事实证实一些旧的习惯是错误的。
双重检查锁定是为了不同步的资源消耗。例如twoprint程序可能会错误的写成:

var a string
var done bool

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

func doprint() {
    if !done {
        once.Do(setup)
    }
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

doprint中看到done被赋值并不保证能看到对a赋值。此程序可能会错误地输出空字符而不是"hello, world"。
另外一个错误的习惯是忙等待 例如:

var a string
var done bool

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

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

和以前程序相似,在main中看到done被赋值不能保证看到a被赋值,因此此程序也可能打印出空字符。更糟糕的是由于两个线程间没有同步事件,在main中可能永远不会看到done被赋值,因此main中的循环不保证能结束。
对程序作一个微小的改变:

type T struct {
    msg string
}

var g *T

func setup() {
    t := new(T)
    t.msg = "hello, world"
    g = t
}

func main() {
    go setup()
    for g == nil {
    }
    print(g.msg)
}

即便main看到了g != nil而且退出了循环,也不能保证看到g.msg的初始化值。 在上面全部的例子中,解决办法都是相同的:明确的使用同步。

相关文章
相关标签/搜索