天天看点

077-内存同步

还记得前面的银行账户存款的例子吗?为了解决竞争问题,我们曾经使用了 sync.Mutex 来保护共享变量。

特别的,在读取 balance 变量的时候,我们是这样写的:

func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return      

当时我们说到,这里加锁需要进一步讨论。这种情况也是并发编程里让人感到相当困惑的地方。

1. 例子

我们来看一个简单的例子。

var x, y int

// goroutine A
go func() {
    x = 1
    fmt.Printf("y: %d ", y)
}()

// goroutine B
go func() {
    y = 1
    fmt.Printf("x: %d ", x)
}()      

仅通过分析,我们能预见的可能的结果有:

y: 1 x: 1
x: 0 y: 1
y: 1 x: 0
x: 1 y: 1      

然而,实际在运行的时候,可能还会出现下面的结果:

x: 0 y: 0
y: 0 x: 0      

这已经不是传统意义上的编译器和 cpu 的工作方式了。这个问题不只会出现在 Golang 里,同样也会出现在你所编写的 C/C++ 的程序里。(并发编程是一件挑战性极高的事,不是说加个锁你的程序就没问题了。)

上面的结果要怎么解释呢?

1.1 指令重排

一般来说,在一个 goroutine 里,每条语句的执行顺序都是保证的。但是,在不使用 channel 且不使用 mutex 这样的显式同步操作的时候,我们就没办法保证每条语句的执行顺序(指令重排)。

这样一来,编译器就有可能在编译这两个 goroutine 的时候,就有可能将赋值操作放到 print 语句后面。

1.2 内存同步

除了编译器指令重排可能导致这种问题外,在多核处理器中,还会有主存同步的问题。

现代处理器为了提高运行效率,每个核心都有自己的 Local Cache,并且在必要的时候,将其 commit 到内存(主存同步)。

在 Golang 里像 channel 通信,互斥操作这样的原语都会触发“主存同步”。只有这样,在一个 goroutine 里的执行结果,才能被运行在其它 cpu 或其它核心的 goroutine 所看到。

2. 总结

  • 主存同步
  • 内存一致性
  • 指令重排