Golang内存模型定义了在一个并发程序中,多个Goroutine对共享变量访问的行为。这里简单介绍一下我对Golang内存模型的理解。
基础理解
在Golang中,如果一个Goroutine在没有同步操作的情况下观察到另一个Goroutine的操作结果,那这种行为是没有保障的。也就是说,如果你要在多个Goroutine之间共享数据,你应该使用某种形式的同步。
考虑以下例子:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
在上面的代码中,main函数可能会打印“hello, world”,也可能打印一个空字符串,甚至可能陷入无限循环,因为Goroutine中的操作可能对main函数不可见。
同步操作
为了在Goroutine之间正确地同步,你需要使用channel,或者使用sync包中的功能,如Mutex或WaitGroup。
以下是一个使用channel来同步的例子:
var a string
func setup() {
a = "hello, world"
}
func main() {
done := make(chan bool)
go func(){
setup()
done <- true
}()
<-done
print(a)
}
在这个例子中,main函数在打印a之前会等待setup函数完成,所以它总是打印出"hello, world"。
顺序一致性
Golang保证在单个Goroutine中,读和写操作会像在顺序一致的系统中一样执行。但是,不同Goroutine之间的操作顺序是由同步事件决定的。
示例:
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
在这个例子中,即使在f函数中a是在b之前设置的,g函数也可能先打印出b的值2,然后打印出a的值0。
Happens Before原则
在Golang中,理解并发编程和内存模型的关键是理解"happens before"的原则。如果在代码中,事件A happens-before事件B, 那么在所有的处理器上,事件A的影响对事件B都是可见的。
示例:
var a string
var done = make(chan bool)
func hello() {
a = "hello, world"
close(done)
}
func main() {
go hello()
<-done
print(a)
}
在上述代码中,我们可以保证hello函数对a的写入操作 happens-before main函数中读取a的操作,因为写入操作在done通道关闭之前发生,而读取操作在done通道关闭后发生。
数据竞争与原子操作
同一个变量同时被多个goroutine读取和写入,而且他们之间没有明显的happens-before关系,这就会产生数据竞争。数据竞争可能会导致不可预见的结果,因此应该避免。
Golang的sync/atomic包提供了一系列原子操作,可以在并发环境中保证单一读/写操作是安全的。
示例:
var count uint64
func increment() {
atomic.AddUint64(&count, 1)
}
func main() {
for i := 0; i < 10; i++ {
go increment()
}
time.Sleep(time.Second)
finalCount := atomic.LoadUint64(&count)
fmt.Println(finalCount)
}
在这个例子中,尽管有多个goroutine同时调用increment函数,但是对count的增加操作是原子的,不存在数据竞争。
总结
理解Golang的内存模型对于编写正确的并发代码至关重要。总的来说,我们应该避免在没有同步的情况下在多个goroutine之间共享变量,要理解和利用happens-before的原则,避免数据竞争,并有效地使用原子操作和同步原语。