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.
查看源码,进一步立即实现机制
代码语言:javascript复制const (
...
hashWriting = 4 // a goroutine is writing to the map
...
)
type hmap struct {
...
flags uint8
...
}
map是检查是否有另外线程修改h.flag来判断,是否有并发问题。
代码语言:javascript复制// 在更新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不停地读
代码语言:javascript复制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)
}
立马产生错误
代码语言:javascript复制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.
代码语言:javascript复制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()
针对上面有并发问题的测试例子,可以修改成以下代码:
代码语言:javascript复制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
代码语言:javascript复制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
参考链接
- The Go Blog - Go maps in action
- Why are map operations not defined to be atomic?