美文网首页
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 内存模型简要说明

    go 内存模型 大体上来说go的内存是先申请一大片内存,然后将内存分为各个小的span来管理,因为每个go对象有对...

  • go的内存模型

    Go内存模型 介绍 Go的内存模型指定了一个条件,在该条件下,可以保证在一个goroutine中读取便利,以观察通...

  • Go——内存模型

    什么是内存模型 The Go memory model specifies the conditions unde...

  • Go的内存模型

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

  • Go 的内存模型

    介绍 Go 的内存模型是可以让多个 goroutine 共享数据的,但指定了条件,在这种条件下,保证一个 goro...

  • Go 内存模型(The Go Memory Model)

    原文链接:https://golang.org/ref/mem,2014.05.31日的版本。自翻,仅供参考。 I...

  • go-map源码简单分析(map遍历为什么时随机的)

    GO 中map的底层是如何实现的 首先Go 语言采用的是哈希查找表,并且使用链表解决哈希冲突。 GO的内存模型 先...

  • golang并发总结

    golang并发模型 go在语言层面提供了内置的并发支持 不要通过共享内存来通信,而应该通过通信来共享内存 并发与...

  • 白话Go内存模型&Happen-Before

    来自公#众#号:Gopher指北[https://isites.github.io/] Go内存模型明确指出,一个...

  • Flink JobManager | TaskManager内存

    Flink内存模型分析 JobManager内存模型 TaskManager内存模型 内存模型分析 Flink使用...

网友评论

      本文标题:Go 的内存模型

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