map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.
具体规则是:
(1)并发读没有问题;
(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;
(3)读的时候发现其他 goroutine 在并发写,抛出fatal("concurrent map read and map write")
(4)写的时候发现其他 goroutine 在并发写,抛出fatal("concurrent map writes")
需要关注,此处并发读写会引发 fatal error,是一种比 panic 更严重的错误,无法使用 recover 操作捕获.
recover哪些情况下阻止不了程序崩溃?
来自 Golang map 实现原理
map不是并发安全的!
并发安全也称线程安全,如果在并发中出现了数据的丢失或错乱,就是并发不安全
golang的map不是并发安全的,并发对map读写可能会报fatal error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "time" ) const N = 5 func main () { m := make (map [int ]int ) go func () { for i := 0 ; i < N; i++ { m[i] = i*10 + 6 } }() go func () { for i := 0 ; i < N; i++ { fmt.Println(i, m[i]) } }() time.Sleep(1e9 * 5 ) }
输出为:
上面第一个协程对map写,第二个对map读,
注:如上代码中,调换两个协程的顺序,并不会对结果有影响
当 N 较大如等于1000时,该程序将报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 fatal error: concurrent map read and map write goroutine 18 [running]: runtime.throw(0x10cb62d, 0x21) /usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc00002ef30 sp=0xc00002ef00 pc=0x10281e2 runtime.mapaccess1_fast64(0x10ab340, 0xc000084000, 0x0, 0x0) /usr/local/go/src/runtime/map_fast64.go:21 +0x1b3 fp=0xc00002ef58 sp=0xc00002ef30 pc=0x100eac3 main.main.func2(0xc000084000) /Users/shuangcui/go/note/map/1.go:22 +0x67 fp=0xc00002efd8 sp=0xc00002ef58 pc=0x1093c37 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc00002efe0 sp=0xc00002efd8 pc=0x1051b51 created by main.main /Users/shuangcui/go/note/map/1.go:20 +0x6a goroutine 1 [sleep]: runtime.goparkunlock(...) /usr/local/go/src/runtime/proc.go:307 time.Sleep(0x12a05f200) /usr/local/go/src/runtime/time.go:105 +0x159 main.main() /Users/shuangcui/go/note/map/1.go:26 +0x7d goroutine 17 [runnable]: main.main.func1(0xc000084000) /Users/shuangcui/go/note/map/1.go:16 +0x45 created by main.main /Users/shuangcui/go/note/map/1.go:14 +0x48 exit status 2
解决方案:
加锁实现
可以用sync.Mutex (互斥锁),或者sync.RWMutex(读写锁)来”重构”map,
即定义一个新的结构体,为该结构体实现两个方法,在方法里使用sync包的Mutex或RWMutex来做加锁保证并发安全.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "fmt" "time" "sync" ) const N = 1000 type Cmap struct { m map [int ]int lock *sync.RWMutex } func (c *Cmap) Get(key int ) int { c.lock.RLock() defer c.lock.RUnlock() return c.m[key] } func (c *Cmap) Set(key, val int ) { c.lock.Lock() defer c.lock.Unlock() c.m[key] = val*10 + 8 } func main () { m := make (map [int ]int ) lock := new (sync.RWMutex) cm := Cmap{ m: m, lock: lock, } go func () { for i := 0 ; i < N; i++ { fmt.Println(i, cm.Get(i)) } }() go func () { for i := 0 ; i < N; i++ { cm.Set(i, i) } }() time.Sleep(1e9 * 5 ) }
直接使用sync.Map
从 go1.9 开始,新增了sync.Map,对并发情况下map的安全做了支持,可以对上面代码进行简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "sync" "fmt" "time" ) const N = 1000 func main () { var m sync.Map go func () { for i := 0 ; i < N; i++ { v, _ := m.Load(i) fmt.Println(i, v) } }() go func () { for i := 0 ; i < N; i++ { m.Store(i, i*10 +4 ) } }() time.Sleep(5 * 1e9 ) }
Store为写,Load为读
除去Store和Load两个方法,sync.Map还有几个方法:
func (m *Map) Delete(key interface{})
func (m *Map) Range(f func(key, value interface{}) bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 这个方法有点绕,举例说下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" "sync" ) func main () { var m sync.Map actual, loaded := m.LoadOrStore("k1" , "v1" ) fmt.Println(actual, loaded) actual, loaded = m.LoadOrStore("k1" , "v1" ) fmt.Println(actual, loaded) actual, loaded = m.LoadOrStore("k1" , "v2" ) fmt.Println(actual, loaded) actual, loaded = m.LoadOrStore("k2" , "v2" ) fmt.Println(actual, loaded) }
LoadOrStore方法其实是先取, 如果有key的话就返回,没有再存储,即优先取,没有再存储;
该方法有两个返回值,第一个是键值,第二个是之前有无该键名(bool类型)
(和缓存的做法很类似:先从缓存中取,如果没有则从数据库中读,读回来存入缓存,下次就可以从缓存中取到了。)
原文链接: https://dashen.tech/2019/01/18/golang之map并发访问/
版权声明: 转载请注明出处.