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.

摘自: go-maps-in-action

并发读写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
也用了相同的办法来实现。

source

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
更优。
所以目前我的结论是:

对于并发读写需求的 map,使用 shard 优化了的 RWLockMap 就足够好了。