竞态

并发安全
如果函数在并发调用时仍能正确工作,那么这个函数是并发安全的。
如果一个类型所有操作都是并发安全的,则称为并发安全的类型。

竞态
指多个 goroutine 交错执行时有时不能给出正确结果。

var count int
// 计数器
func Add()  {
	// 非并发安全,存在数据竞态
	count = count + 1
}

数据竞态
发生于多个 goroutine 并发读写同一个变量且至少一个是写入时。

有三种解决方法:

  • 不修改变量,只读取
  • 限制只有一个 goroutine 访问变量
  • 通过互斥机制
互斥锁 sync.Mutex
var (
	mu sync.Mutex
	count int
)
// 并发安全的计数器
func Add() {
	mu.Lock()
	defer mu.Unlock()
	count = count + 1
}

go 的互斥锁是不可重入的。
Lock 方法获取锁,若此时其他 goroutine 获取了锁,会一直阻塞到其他 goroutine 调用 Unlock。
函数可能非常复杂,采用 defer 保证每个分支都执行了 Unlock,尤其是错误分支。

读写互斥锁 sync.RWMutex

多读单写锁
读操作之间可以并发,读操作和写操作之间、多个写操作之间互斥。

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