面试题答案
一键面试在Go语言中,虽然不同的goroutine操作不同的变量在大多数直观场景下似乎不会产生数据竞争,但由于Go语言的内存模型特性,依然可能出现数据竞争问题,原因如下:
- 缓存一致性问题:现代CPU为了提高性能,每个核心都有自己的缓存。当一个goroutine在某个核心上修改了变量,这个修改可能暂时只存在于该核心的缓存中,其他核心的缓存还是旧值。如果另一个goroutine在其他核心上读取该变量,就可能读到旧值,造成数据不一致。
- 重排序优化:编译器和CPU为了优化性能,可能会对指令进行重排序。在单线程环境下,重排序不会影响程序逻辑,但在并发环境中,重排序可能导致不同goroutine之间观察到的操作顺序与代码编写顺序不一致,从而引发数据竞争。
示例代码如下:
package main
import (
"fmt"
"sync"
)
var (
a int
b int
)
func write() {
a = 1
b = 1
}
func read() {
for {
if b == 1 {
fmt.Println(a)
}
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
write()
}()
go func() {
defer wg.Done()
read()
}()
wg.Wait()
}
在上述代码中,write
函数和read
函数操作的是不同变量a
和b
。按照正常逻辑,当read
函数读到b == 1
时,a
也应该已经被赋值为1。但由于指令重排序,write
函数中a = 1
和b = 1
的执行顺序可能被改变,导致read
函数读到b == 1
时,a
还未被赋值为1,从而打印出0,出现数据竞争问题。
为了避免这种数据竞争,在Go语言中通常使用sync.Mutex
、sync.RWMutex
等同步机制,或者使用channel
来进行数据通信和同步。例如:
package main
import (
"fmt"
"sync"
)
var (
a int
b int
mu sync.Mutex
)
func write() {
mu.Lock()
a = 1
b = 1
mu.Unlock()
}
func read() {
mu.Lock()
if b == 1 {
fmt.Println(a)
}
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
write()
}()
go func() {
defer wg.Done()
read()
}()
wg.Wait()
}
使用sync.Mutex
后,确保了write
和read
函数对共享变量的操作是原子性的,避免了数据竞争。