美文网首页
Go 的内存模型

Go 的内存模型

作者: Robin92 | 来源:发表于2020-04-18 13:32 被阅读0次

    介绍

    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):

    • 读不发生在写之前;
    • 在此写操作之后或此读之前没有其他的写操作(w')发生。

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

    • 写发生在读之前;
    • 其他任何对 v 的写操作(w', w''...),或者发生在 w 之前,或者发生在 r 之后。

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

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

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

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

    同步(Synchronization)

    初始化(Initialization)

    • 程序初始会运行一个单协程,但这个协程可能会创建并发执行的其他协程。
    • 如果 p 包引入了 q 包,那 q 的 init() 函数发生在 p 的任何操作之前(包括 p 的 init())。
    • main.main()(main 包下的 main())发生在所有的 init() 函数之后。

    协程的创建

    • go 命令开启一个 go 协程发生在此 go 协程执行之前。(即:先创建再执行。这句话的意思是说 go 协程创建之前的语句和这个 go 协程没有并发问题)

    协程的销毁

    • 协程的退出不保证发生在程序的任何事情之前。(有点拗口,就是指协程的执行时间和退出正常情况下完全独立,没有任何时间保证)

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

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

    用 channel

    • 向 channel 中写数据发生在 对应 的读操作之前(即读时保证先完成写操作);
    • channel 的关闭操作发生在读操作之前,由于通道关闭,返回它对应类型的零值;
    • 从无缓冲的 channel 中读操作,发生在向此 channel 的写完成之前;

    用锁

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

    • Lock() 之后,并发协程中的 Unlock() 一定发生在其他协程 Lock() 之前

    用 Once

    var once sync.Once
    ...
    func doprint() {
        once.Do(setup)
        print(setup)
    }
    
    • Once 保证了 once.Do() 只执行一次

    总结

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

    • Go 的 goroutine 是共享内存的;
    • Happends-before 原则;
    • 编译器和执行器会对编码的执行顺序进行重排,但在单协程中对外表现一致;
    • 学会用几种方式保证协程中读写的顺序(与自己期望的一致);
    • 使用显示的同步做同步!!!

    不正确的同步:

    var a string
    var done bool
    
    func setup() {
        a = "hello, world" // 
        done = true        // 由于这两个的赋值操作不一定那个先
    }
    
    func main() {
        go setup()
        for !done {
        }
        print(a)
    }
    

    附:

    相关文章

      网友评论

          本文标题:Go 的内存模型

          本文链接:https://www.haomeiwen.com/subject/dmxvvhtx.html