Go map 的特殊特性

服务器

浏览数:149

2019-9-7

零值特性

未初始化的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 锁

首先,了解一下gomap的基础特性: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把力度更加小的锁的方式实现。

例如 concurrent_map 以及更早的 syncmap

一开始项目里面是使用的 syncmap。 但是这个库有一个致命的缺陷(go-maps-in-action里面的例子也有这个问题)。

考虑这样的一个场景:

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 保护住了用户函数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.Muteatomic 来实现类似
  • 使用 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 就足够好了。

作者:秦川