Go map 的特殊特性
零值特性
未初始化的 map
某些操作是合法的:
var testMap map[int]int size := len(testMap) // size is 0 _, present := testMap[0] // present is false non := testMap[123] // non is 0, won't case panic testMap[1] = non // panic: assignment to entry in nil map
只有写入操作才会出 panic
。
临时迭代obj的地址问题。
迭代 map 和 slice 等容器的时候,不可使用临时变量的地址!
简单的说,这个obj使用的是同一个地址,如果将此变量的地址保存起来,那么将是一个灾难。
代码片段:
var(gSlice []*obj) func foosp(v *obj) { gSlice = append(gSlice, v) } for _, obj := range objSlice { foosp(&obj) }
在 for
循环中,会让调用者产生一种『已经将objSlice中的每个值传递出去』的假象,而实际上对于外部函数接收到的只是同一个地址。当每次 foosp()
刚开始执行的时候, *(&obj)
的值确实是 objSlice
中的当前元素的值,但整段代码执行完, gSlice
中将会是指向同一个地址的、值为 gSlice[-1]
指针。
map 锁
首先,了解一下 go
的 map
的基础特性: hash
实现, key
需要可比较( comparable
),不会自动初始化。
Maps are not safe for concurrent use: it's not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex.
并发读写map的时候一定要加锁,可以用读写锁,并发的读取目前看来是没有问题的。
一个很大的 map
,如果使用一个 sync.RWMutex
,同时,如果有很多routine都在使用的话,那么性能肯定是要吃亏的,于是有人写了一个把一把锁分成N把力度更加小的锁的方式实现。
例如 []( https://github.com/orcaman/co…
更早的 syncmap
一开始项目里面是使用的 syncmap
。 但是这个库有一个致命的缺陷(go-maps-in-action里面的例子也有这个问题)。
考虑这样的一个场景:
graph LR A(Routine A.1)-->A2("A.2 检查map.Get(key)是否存在") A2 --> A3("A.3 写入map.Set(key, vA)") B(Routine B.1)-->B2("B.2 检查map.Get(key)是否存在") B2 --> B3("B.3 写入map.Set(key, vB)")
A.2 A.3 B.2 B.3
这4个操作确实是被锁保护了的,但是我们要的实际上是把
A.2 & A.3
,以及 B.2 & B.3
这两个独立的操作都保护起来。
如果 A.2 B.2 执行之后,A.3和B.3可能会相互覆盖。
我们项目中有这样的需求,于是有同事就加入了一个 Update 的函数来做这个:
func (m *SyncMap) Update(key Key, f func(value interface{}) interface{}) { shard := m.locate(key) shard.Lock() v := shard.items[key] r := f(v) if r != nil { shard.items[key] = r } else { delete(shard.items, key) } shard.Unlock() return }
通过传入一个函数的方法,使得 shard.Lock
保护住了用户hanshu f做的代码的事情。
后面 github 上面的新的 concurrent_map
也用了相同的办法来实现。
type UpsertCb func(exist bool, valueInMap interface{}, newValue interface{}) interface{} // Insert or Update - updates existing element or inserts a new one using UpsertCb func (m ConcurrentMap) Upsert(key string, value interface{}, cb UpsertCb) (res interface{}) { shard := m.GetShard(key) shard.Lock() v, ok := shard.items[key] res = cb(ok, v, value) shard.items[key] = res shard.Unlock() return res }
在 Go 1.7 以后,提供了一个语言级别(内置库)的 sync.Map
来实现并发读写 Map
。
关于 sync.Map
有一篇非常棒的中文博客介绍:
Go 1.9 sync.Map揭秘
通过博客和阅读代码(以及注释)我们知道了实现方式:
-
使用一个冗余的
map
来做到大部分读取操作无锁。 -
使用
sync.Mute
和atomic
来实现类似 -
使用 miss 计数来翻转读和新写入的
map
以达到更优性能。
按照描述里面写的,在每个 key 只会写入一次的场景下性能非常棒(因为几乎完全没有用到任何 Mutex
)。
在 Go 源码的 libexec/src/sync/map_bench_test.go
有作者们自己的性能测试,我在自己机器上(Intel i7-4770HQ 16G DDR3)上测试的结果如下:
BenchmarkLoadMostlyHits/*sync_test.DeepCopyMap-8 100000000 19.7 ns/op BenchmarkLoadMostlyHits/*sync_test.RWMutexMap-8 20000000 60.2 ns/op BenchmarkLoadMostlyHits/*sync.Map-8 100000000 18.0 ns/op BenchmarkLoadMostlyMisses/*sync_test.DeepCopyMap-8 100000000 14.7 ns/op BenchmarkLoadMostlyMisses/*sync_test.RWMutexMap-8 20000000 62.5 ns/op BenchmarkLoadMostlyMisses/*sync.Map-8 100000000 12.4 ns/op BenchmarkLoadOrStoreBalanced/*sync_test.RWMutexMap-8 3000000 549 ns/op BenchmarkLoadOrStoreBalanced/*sync.Map-8 3000000 421 ns/op BenchmarkLoadOrStoreUnique/*sync_test.RWMutexMap-8 2000000 896 ns/op BenchmarkLoadOrStoreUnique/*sync.Map-8 2000000 880 ns/op BenchmarkLoadOrStoreCollision/*sync_test.DeepCopyMap-8 200000000 7.85 ns/op BenchmarkLoadOrStoreCollision/*sync_test.RWMutexMap-8 10000000 181 ns/op BenchmarkLoadOrStoreCollision/*sync.Map-8 200000000 9.02 ns/op BenchmarkRange/*sync_test.DeepCopyMap-8 300000 4325 ns/op BenchmarkRange/*sync_test.RWMutexMap-8 30000 58693 ns/op BenchmarkRange/*sync.Map-8 300000 4169 ns/op
最后还有针对 sync.Map
实现细节的针对性测试( AdversarialAlloc
/ Delete
),我觉得意义不大就忽略了。
这里测试了3种并发访问的 map
:
sync.Map
其中,第三种每次 deep copy 的方式实际上肯定是不敢用的(COPY 整个 map
的操作实在太可怕),这里只是做一个对比,作者可能觉得在某些 key 较少的情况下,它的表现会比较类似原生 map
?
测试结果 sync.Map
比较优的几个场景:
- 并发读写冲突大量产生的时候(避免了锁)。
- range的时候。
实际上那么极端的读写冲突我觉得根本就不会有嘛,如果有类似的场景肯定修改逻辑了哈哈。
通常的读写由于避免了(读写)锁,性能也要优几倍,但是我比较怀疑是否能比设置了适当shard值的 RWMutextMap
更优。
所以目前我的结论是: