golang中的map并发读写问题: Golang 协程并发使用 Map 的正确姿势

时间:2022-10-08 08:58:16


map 不是并发安全的

官方的faq里有说明,考虑到有性能损失,map没有设计成原子操作,在并发读写时会有问题。

Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

查看源码,进一步立即实现机制



const (
...
hashWriting = 4 // a goroutine is writing to the map
...
)

type hmap struct {
...
flags uint8
...
}



map是检查是否有另外线程修改h.flag来判断,是否有并发问题。



// 在更新map的函数里检查并发写
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}

// 在读map的函数里检查是否有并发写
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}



测试并发问题的例子:一个goroutine不停地写,另一个goroutine不停地读



package main

import (
"fmt"
"time"
)

func main() {
c := make(map[string]int)
go func() { //开一个goroutine写map
for j := 0; j < 1000000; j++ {
c[fmt.Sprintf("%d", j)] = j
}
}()
go func() { //开一个goroutine读map
for j := 0; j < 1000000; j++ {
fmt.Println(c[fmt.Sprintf("%d", j)])
}
}()
time.Sleep(time.Second * 20)
}



立马产生错误



0
fatal error: concurrent map read and map write

goroutine 19 [running]:
runtime.throw(0x10d6ea4, 0x21)
/usr/local/go/src/runtime/panic.go:774 +0x72 fp=0xc00009aef0 sp=0xc00009aec0 pc=0x10299c2
runtime.mapaccess1_faststr(0x10b41e0, 0xc000066180, 0x116ae71, 0x1, 0x1)
/usr/local/go/src/runtime/map_faststr.go:21 +0x44f fp=0xc00009af60 sp=0xc00009aef0 pc=0x100ffff
main.main.func2(0xc000066180)



加sync.RWMutex来保护map

This statement declares a counter variable that is an anonymous struct containing a map and an embedded sync.RWMutex.



var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
To read from the counter, take the read lock:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
To write to the counter, take the write lock:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()



针对上面有并发问题的测试例子,可以修改成以下代码:



package main

import (
"fmt"
"sync"
"time"
)

func main() {
var c = struct {
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}

go func() { //开一个goroutine写map
for j := 0; j < 1000000; j++ {
c.Lock()
c.m[fmt.Sprintf("%d", j)] = j
c.Unlock()
}
}()
go func() { //开一个goroutine读map
for j := 0; j < 1000000; j++ {
c.RLock()
fmt.Println(c.m[fmt.Sprintf("%d", j)])
c.RUnlock()
}
}()
time.Sleep(time.Second * 20)
}



第三方 map 包

第三方包的实现都大同小异,基本上都是使用分离锁来实现并发安全的,具体分离锁来实现并发安全的原理可参考下面的延伸阅读

concurrent-map



m:= cmap.New()
//写
m.Set("foo", "hello world")
m.Set("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Set("int", 1)
//读
m.Get("foo")
m.Get("slice")
m.Get("int")
go-concurrentMap

m := concurrent.NewConcurrentMap()
m.Put("foo", "hello world")
m.Put("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Put("int", 1)
//读
m.Get("foo")
m.Get("slice")
m.Get("int")



sync.Map

sync.Map 是官方出品的并发安全的 map,他在内部使用了大量的原子操作来存取键和值,并使用了 read 和 dirty 二个原生 map 作为存储介质,具体实现流程可阅读相关源码。

参考:​​https://learnku.com/articles/27691​

参考链接

  1. ​The Go Blog - Go maps in action​
  2. ​Why are map operations not defined to be atomic?​