Go 的内存模型

2020-04-18  本文已影响0人  Robin92

介绍

Go 的内存模型是可以让多个 goroutine 共享数据的,但指定了条件,在这种条件下,保证一个 goroutine 中可以读取另一个 goroutine 中写入的变量值。

建议

程序中有多个 goroutine 同时去更新数据时,必须用序列化的方式。比如用 channel 操作或 sync、sync/atomic 的同步原语。

意思是:需要同步的场景,使用显式的同步!显式的同步!!显式的!!!

Happens Before

Happens-before 是一个顺序规范,准确地说是规定了部分顺序,让某些事件发生在另一些事件之前。

单个 goroutine

在单个 goroutine 中,读和写操作必须 表现得像 是他们是按程序的执行顺序进行的。

也就是说,实际上,在单个 goroutine 中编译器和执行器会对读操作和写操作的顺序进行重排,但它保证了这个重排不会改变 语言规范 中所定义的行为。

由于重排,一个 goroutine 观察到的执行顺序可能与另一个 goroutine 的不同。
比如,一个 goroutine 执行 a = 1; b = 2,那另一个 goroutine 可能先观察到 b == 2 而此时 a != 1。如下测试代码:

var count = new(uint64)

func main() {
    for i := 0; i < 1000000; i++ {
        test()
    }
    time.Sleep(time.Second)
    fmt.Println(*count) // 输出不为 0 
}
func test() {
    var a, b int
    go func() {
        A, B := a, b
        if B == 2 && A != 1 {
            atomic.AddUint64(count, 1)
        }
    }()
    go func() {
        a = 1
        b = 2
    }()
}

为了指定读和写的要求,我们在 Go 中定义了 Happens before 的概念——Go 执行内存操作的部分顺序。
如果事件 e1 在 e2 之前发生,我们可以说 e2 发生在 e1 之后。如果 e1 即不在 e2 之前发生也不在 e2 之后发生,我们称为 e1 和 e2 是同时发生。

在单 goroutine 中,happens-before 顺序就是程序语言所表达的顺序。

当满足以下两个条件时,一个变量(v)的读操作(r)允许观察它的写操作(w):

为了保证上述读操作(r)能观察到上述的写操作(w),即需要保证以下两个条件:

这两个条件比上面的两个条件更健壮。它要求了这里没有其他的写操作与这里的 r 和 w 并发。

在单协程中没有并发,所以读操作(r)总能观察到最近一次写操作(w)的值。

而在多协程中,我们就必须用同步事件来构建 happens-before 条件来确保读观察到了期望的写入。

对于一个变量初始化为它的零值时,表现为在内存模型中的一个写操作。
对一个大于 单机器字 的读和写操作表现为对一个未指明顺序的 多机器字大小(multiple machine-word-sized)的操作。

同步(Synchronization)

初始化(Initialization)

协程的创建

协程的销毁

(我们初学协程时,常常会写 main 函数的最后一句创建 go 协程,结果导致主程序结束了协程还没执行。就是这个意思)

确保并发下同步的三种方式

用 channel

用锁

sync 包提供了两种锁 sync.Mutexsync.RWMutex

用 Once

var once sync.Once
...
func doprint() {
    once.Do(setup)
    print(setup)
}

总结

这里注意的内容主要有以下几点:


不正确的同步:

var a string
var done bool

func setup() {
    a = "hello, world" // 
    done = true        // 由于这两个的赋值操作不一定那个先
}

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

附:

上一篇下一篇

猜你喜欢

热点阅读