Map数据竞争
直接看一段例子
package main import ( "sync" "fmt" ) func main() { m := map[int]int{} var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { if _, exists := m[i]; !exists { m[i] = i } wg.Done() }(i) } wg.Wait() fmt.Println(m) }
这个例子可能有三种输出情况
可能输出情况
情况1
输出结果:正常
map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]
情况2
报fatal,错误信息显示存在读和写冲突
fatal error: concurrent map read and map write
情况3
报fatal,错误信息显示写冲突
fatal error: concurrent map writes
原因
map在go的内部实现其实是哈希表,哈希表结构体定义。重点关注flags字段。这个字段在处理冲突中起了关键作用。
type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // optional fields }
1)写操作的时候会判断,并且写一下flags值
2)读操作也有这样的判断,和写操作的判断是一样的
快速发现
数据竞争出现的时候,并不是100%出现异常,也可能运行之后出现正常的情况,从而会误判程序没有问题
使用-race参数可以快速检测数据冲突的风险
xxxxxxxx@B000000400110Z ~ % go run -race /tmp/test4.go ================== WARNING: DATA RACE Read at 0x00c000124180 by goroutine 8: runtime.evacuate_fast32()
解决方案
解决方案1
使用锁实现map原子操作
package main import ( "fmt" "sync" ) type myMap struct { sync.RWMutex m map[int]int } func main() { mp := myMap{} mp.m = map[int]int{} var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { mp.Lock() defer mp.Unlock() if _, exists := mp.m[i]; !exists { mp.m[i] = i } wg.Done() }(i) } wg.Wait() fmt.Println(mp.m) }
解决方案1
在go1.9之后,使用sync.Map{}实现map的原子操作
package main import ( "fmt" "sync" ) func main() { m := sync.Map{} var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { if _, exists := m.Load(i); !exists { m.Store(i, i) } wg.Done() }(i) } wg.Wait() m.Range(func(key, value interface{}) bool { fmt.Println(key, value) return true }) }
Slice数据竞争
通常对 map 的并发读写问题很容易发现,因为一旦并发的更新一个 map 时,golang 会 panic,你可以从日志中很快发现这个问题。但是对 slice 进行 append 时也有并发安全问题,但是由于不会 panic 因此很容易被忽略,一旦没有对比校验实际数据量,很难发现问题。
这种情况目前只能用下面两种方式发现
可能输出情况
1)-race参数运行,但是这会造成5-10倍的性能损失,没有人会在线上开启,而且开启了也一定会执行到那块逻辑
2)校验运行结果和预期的结果,这个一般也很难做到,除非有意识去做测试
先来看一段程序可能的输出:
package main import ( "fmt" "sync" ) var globle []int func main() { globle = make([]int, 10) wg := sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { go func() { globle = append(globle, 1) wg.Done() }() } wg.Wait() fmt.Println(globle) }
这段程序可能输出
xxxxx@192 ~ % for ((i=0; i<10; i++)) do go run /tmp/test.go done [0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1] [0 0 0 0 0 0 0 0 0 0 1] [0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1] [0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1] [0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1] [0 0 0 0 0 0 0 0 0 0 1 1 1 1]
快速发现
可以看到,输出结果是不确定的,我么用-race检测数据竞争,注意,slice发生数据竞争的时候是没有fatal或者panic
xxxx@192 ~ % go run -race /tmp/test.go ================== WARNING: DATA RACE Read at 0x000104cfec50 by goroutine 8: main.main.func1() /tmp/test.go:17 +0x34 Previous write at 0x000104cfec50 by goroutine 7: main.main.func1() /tmp/test.go:17 +0xa8 Goroutine 8 (running) created at: main.main() /tmp/test.go:16 +0xcc Goroutine 7 (finished) created at: main.main() /tmp/test.go:16 +0xcc ==================
解决方案
使用map锁机制实现并发安全
package main import ( "fmt" "sync" ) var globle []int func main() { globle = make([]int, 10) var mu sync.Mutex wg := sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { go func() { mu.Lock() globle = append(globle, 1) mu.Unlock() wg.Done() }() } wg.Wait() fmt.Println(globle) }
No Leanote account? Sign up now.