Go内存更新问题

前言

在开始之前, 先来引出问题. 有这样一段go代码:

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    var x, y int
    go func() {
        defer wg.Done()
        x = 1
        fmt.Println(fmt.Sprintf("y=%d", y))
    }()
    go func() {
        defer wg.Done()
        y = 1
        fmt.Println(fmt.Sprintf("x=%d", x))
    }()
    wg.Wait()
}

这段代码可能会有哪些结果呢? 无非看语句的执行顺序嘛, 排列组合一下可能的情况:

  • x=1, y=1
  • x=0, y=1
  • x=1, y=1

但是, 如果你多跑几次, 就会发现, x=0, y=0这种我们以为不会出现的情况, 是真的会出现的.

不过这种情况为什么发生想必也都心知肚明, 就简单聊一聊吧.

why

为什么会出现这种情况呢? 其实, 如果了解多线程及 CPU 的实现, 倒也不难理解.

首先, CPU 为了加速, 存在多级缓存. 同样的内存修改会先修改 CPU 内部缓存, 不会立即刷新到内存上.

此时, 若是多核 CPU, 多个线程跑在不同核上, CPU1对内存进行了修改, 但此时修改还在CPU缓存上, 没有刷新到内存, CPU2到内存读到的就是旧值.

同样的, 在Go中, 多个协程也是跑在不同的 CPU 核上, 所以, 内存的更新对其他 CPU 核来说也不是立即可见的. 出现这样的问题也就不奇怪了.

不光是写, 读操作也是有缓存的呦

探究

Java中, 存在volatile关键字, 来保证字段的更新立即刷新到内存. 那么在Go中, 如何来解决这个问题呢?

一说到多线程同步, 第一个想到的必定就是锁了, 没错lock可以保证更新立即可见, 同样的channel atomic 都可以. 其中atomic包做的事情 和volatile是一样的. (也就是说, 前面的例子只要在将读写的操作改为atomic, 就不会出现 x=0,y=0 的情况啦).

Go官方文档中对内存模型进行了简单的介绍, 也说明了这种错误. 甚至于, 在文档中给出了这样的例子(感兴趣的可以去看一下文档, 还挺有趣的):

var a string 
var done bool 
func setup() { 
    a = "hello, world" 
    done = true 
} 
func main() { 
    go setup() 
    for !done { 
    } 
    print(a) 
}
  1. 结果可能打印空字符串. 也就是说, done的更新同步到内存了, 但是a没有
  2. 甚至, 极端情况main的循环可能不会结束. 即CPU 缓存刷新时间很长.

同时, 在这篇官方文档中还有一些很有意思的内容, 推荐读一读. 比如:

  1. 说明了编译期对指令执行顺序的保证
  2. 多协程通信的方式(就是我们已知的几个lock/channel/atomic等)
  3. runtime.SetFinalizer变量的析构函数(但是如果在回收前进程就结束了, 可能不会调用)
  4. build/run命令后跟上-race参数, 可以检测是否存在多协程变量竞争的问题. 若存在, 会在运行时报错.
  5. 等等

over, 对此问题一个简简单单的回顾

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x