零值特性
未初始化的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
。 - 每次写入都使用 atomic 拷贝以避免使用Mutex或者WLock的方式。
其中,第三种每次 deep copy 的方式实际上肯定是不敢用的(COPY 整个map
的操作实在太可怕),这里只是做一个对比,作者可能觉得在某些 key 较少的情况下,它的表现会比较类似原生map
?
测试结果 sync.Map
比较优的几个场景:
- 并发读写冲突大量产生的时候(避免了锁)。
- range的时候。
实际上那么极端的读写冲突我觉得根本就不会有嘛,如果有类似的场景肯定修改逻辑了哈哈。
通常的读写由于避免了(读写)锁,性能也要优几倍,但是我比较怀疑是否能比设置了适当shard值的RWMutextMap
更优。
所以目前我的结论是:
对于并发读写需求的 map,使用 shard 优化了的 RWLockMap 就足够好了。
有疑问加站长微信联系(非本文作者)