深入理解golang:sync.map

有了map为什么还要搞个sync.map 呢?它们之间有什么区别?
答:重要的一点是,map并发不是安全的。

在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

go version go1.13.9 windows/amd64

测试一波

写一个简单的并发写map的测试代码看看:
testcurmap.go

package main import ( "fmt" "time" ) func main() { m := map[string]int{"age": 10} go func() { i := 0 for i < 1000 { m["age"] = 10 i++ } }() go func() { //19 行 i := 0 for i < 1000 { m["age"] = 11 //22 行 i++ } }() time.Sleep(time.Second * 3) fmt.Println(m) }

多运行几次:go run testcurmap.go
会报错,错误的扼要信息如下:

fatal error: concurrent map writes

goroutine 7 [running]:
runtime.throw(0x4d49a3, 0x15)
       /go/src/runtime/panic.go:774 +0x79 fp=0xc000041f30 sp=0xc000041f00 pc=0x42cf19
runtime.mapassign_faststr(0x4b4360, 0xc000066330, 0x4d168a, 0x3, 0x0)
        /go/src/runtime/map_faststr.go:211 +0x41e fp=0xc000041f98 sp=0xc000041f30 pc=0x410f8e
main.main.func2(0xc000066330)
        /mygo/src/study/go-practice2/map/curmap/testcurmap.go:22 +0x5c fp=0xc000041fd8 sp=0xc000041f98 pc=0x49ac9c
runtime.goexit()
      /go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc000041fe0 sp=0xc000041fd8 pc=0x455391
created by main.main
       /mygo/src/study/go-practice2/map/curmap/testcurmap.go:19 +0xb0

exit status 2

看报错信息是src/runtime/map_faststr.go:211 这个函数runtime.mapassign_faststr,它在runtime/map_faststr.go 中,简要代码如下:

func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer { ... ... if h.flags&hashWriting != 0 { throw("concurrent map writes") } ... ... }

hashWriting  = 4 // a goroutine is writing to the map goroutine写的一个标识,
这里h.flags与自己进行与运算,判断是否有其他goroutine在操作这个map,不是0说明有其他goroutine操作map,所以报错。

那咋防止map并发呢,一般有几种方式:

map+Mutex:
给map加一把大锁

map+RWMutex
给map加一个读写锁,给锁细分。适合读多写少场景

修改一下程序

加一把读写锁防止并发,修改程序 testcurmap2.go:

package main import ( "fmt" "sync" "time" ) func main() { m := map[string]int{"age": 10} var s sync.RWMutex go func() { i := 0 for i < 1000 { s.Lock() m["age"] = 10 s.Unlock() i++ } }() go func() { i := 0 for i < 1000 { s.Lock() m["age"] = 11 s.Unlock() i++ } }() time.Sleep(time.Second * 3) fmt.Println(m) }

运行结果:
map[age:11]

没有报错了。

就到这里了吗?可以在思考思考,还有其他方法控制并发的方法没?有的,sync.map 登场

控制并的第三种方式:

sync.Map
官方实现的并发map。
原理是通过分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化。一般情况下可以替换上面2种锁。

sync.map

先看一个简单的代码 testcurmap3.go

package main import ( "fmt" "sync" "time" ) func main() { smap := sync.Map{} smap.Store("age", 10) go func() { i := 0 for i < 1000 { smap.Store("one", 10) i++ } }() go func() { i := 0 for i < 1000 { smap.Store("one", 11) i++ } }() time.Sleep(time.Second * 2) fmt.Println(smap.Load("one")) }

运行输出:11 true
正常输出,没有报错。

sync.Map 的主要思想就是读写分离,空间换时间

看看 sync.map 优点:

空间换时间:通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。

使用只读数据(read),避免读写冲突。

动态调整,miss次数多了之后,将dirty数据迁移到read中。

double-checking。

延迟删除。 删除一个键值只是打标记,只有在迁移dirty数据的时候才清理删除的数据。

优先从read读取、更新、删除,因为对read的读取不需要锁。

sync.Map 数据结构 Map 数据结构

在 src/sync/map.go 中

type Map struct { // 当涉及到脏数据(dirty)操作时候,需要使用这个锁     mu Mutex // read是一个只读数据结构,包含一个map结构, // 读不需要加锁,只需要通过 atomic 加载最新的指正即可     read atomic.Value // readOnly // dirty 包含部分map的键值对,如果操作需要mutex获取锁 // 最后dirty中的元素会被全部提升到read里的map去     dirty map[interface{}]*entry // misses是一个计数器,用于记录read中没有的数据而在dirty中有的数据的数量。 // 也就是说如果read不包含这个数据,会从dirty中读取,并misses+1 // 当misses的数量等于dirty的长度,就会将dirty中的数据迁移到read中     misses int } read的数据结构 readOnly: // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { // m包含所有只读数据,不会进行任何的数据增加和删除操作 // 但是可以修改entry的指针因为这个不会导致map的元素移动     m       map[interface{}]*entry // 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据     amended bool // true if the dirty map contains some key not in m. }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsffdg.html