并发安全
如果函数在并发调用时仍能正确工作,那么这个函数是并发安全的。
如果一个类型所有操作都是并发安全的,则称为并发安全的类型。
竞态
指多个 goroutine 交错执行时有时不能给出正确结果。
var count int
// 计数器
func Add() {
// 非并发安全,存在数据竞态
count = count + 1
}
数据竞态
发生于多个 goroutine 并发读写同一个变量且至少一个是写入时。
有三种解决方法:
- 不修改变量,只读取
- 限制只有一个 goroutine 访问变量
- 通过互斥机制
var (
mu sync.Mutex
count int
)
// 并发安全的计数器
func Add() {
mu.Lock()
defer mu.Unlock()
count = count + 1
}
go 的互斥锁是不可重入的。
Lock 方法获取锁,若此时其他 goroutine 获取了锁,会一直阻塞到其他 goroutine 调用 Unlock。
函数可能非常复杂,采用 defer 保证每个分支都执行了 Unlock,尤其是错误分支。
多读单写锁
读操作之间可以并发,读操作和写操作之间、多个写操作之间互斥。
var (
mu sync.RWMutex
count int
)
// 并发安全的计数器
func Add() {
// 写锁
mu.Lock()
defer mu.Unlock()
count = count + 1
}
func Get() int {
// 读锁
mu.RLock()
defer mu.RUnlock()
return count
}
只有锁竞争比较激烈,且绝大部分为读操作时,RWMutex 才有性能优势,否则不如 Mutex。
内存可见性func test() {
var x, y int
go func() {
x = 1
fmt.Print("y:", y, " ")
}()
go func() {
y = 1
fmt.Print("x:", x, " ")
}()
}
以上程序可能输出 x:0 y:0
原因有二:
- 由于赋值和 Print 对应不同的变量,编译器可能打乱语句顺序
- 每个CPU内核都有自己的高速缓存,写操作可能只写入了CPU缓存,在同步到内存之前对其他CPU内核不可见
而通道通信和互斥锁会保证一个 goroutine 的写操作对其他 goroutine 可见。
延迟初始化 sync.Once以下是一个延迟初始化的单例模式:
var something *Something
var somethingOnce sync.Once
func GetSomething() *Something {
// initSomething 为初始化 something 的函数
// sync.Once 保证 initSomething 只被调用一次
somethingOnce.Do(initSomething)
return something
}
竞态检测器 (race detector)
将 -race 参数加到 go build、go run、go test 命令中,以开启竞态检测。
如下所示:
go build -race race.go
==================
WARNING: DATA RACE
Write at 0x00c000116010 by goroutine 7:
main.main.func2()
F:/gopath/learnProject/race.go:15 +0x3f
Previous write at 0x00c000116010 by goroutine 6:
main.main.func1()
F:/gopath/learnProject/race.go:9 +0x3f
Goroutine 7 (running) created at:
main.main()
F:/gopath/learnProject/race.go:13 +0xa3
Goroutine 6 (running) created at:
main.main()
F:/gopath/learnProject/race.go:7 +0x81
Found 1 data race(s)
Process finished with exit code 66