Concurrent Map
背景
map是平时项目中经常用到的数据类型,但是如果多个协程去读写同一个map时,为了不发生数据错误,经常去将其和锁封装成一个新的map。像以下两种示例。
type LockMap struct {
m map[interface{}]interface{}
lock sync.Mutex
}
type RwLockMap struct {
m map[interface{}]interface{}
lock sync.RWMutex
}
LockMap中,每次去读或者写时,都会调用Mutex去把map锁住。
RwLockMap中,如果程序中读较多,写较少的情况下,性能会比LockMap高很多,因为读的时候是可以多个协程一起读的。但是如果写较多时,性能可能与LockMap差不多。
这是我们不想看到的,我们想的是,随便读随便写,并且不会发生数据错误。go语言中有一个sync.Map.
sync.Map会有两块区域,一个是只读,一个是可读可写,其中可读可写的区域也是有一个锁的。每次操作时,会先去只读区域中寻找,只读区域未找到的时候触发miss,然后才会去可读可写区域寻找。很显然,最糟糕的情况下比LockMap性能更低,因为比LockMap多一次查找的动作。sync.Map也是适合读多写少的情况。
有没有比较好的方法呢?有,Concurrent Map。
github.com/easierway/concurrent_map
使用map的痛点在于,每次锁住的时候,都是去锁的整个map。Concurrent Map就是减少锁冲突的次数。它把一个map分成很多个小的map,每个map配一个锁。这时锁冲突的概率就大大减少了。
性能
使用Benchmark分别测试一下几中map的性能,分别测试读多写少、读写参半、读少写多三种情况。
代码如下:由于测试步骤一致。使用接口来测试。
type Map interface {
Set(key interface{}, val interface{})
Get(key interface{}) (interface{}, bool)
Del(key interface{})
}
func benchmarkMap(b *testing.B, hm Map) {
b.ResetTimer()
defer b.StopTimer()
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
for j := 0; j < NumOfWriter; j++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
hm.Set(strconv.Itoa(i), i*i)
hm.Del(strconv.Itoa(i))
}
}()
}
for j := 0; j < NumOfReader; j++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
hm.Get(strconv.Itoa(i))
}
}()
}
wg.Wait()
}
}
func BenchmarkSyncmap(b *testing.B) {
b.Run("map with Lock", func(b *testing.B) {
hm := CreateLockMap()
benchmarkMap(b, hm)
})
b.Run("map with RWLock", func(b *testing.B) {
hm := CreateRwLockMap()
benchmarkMap(b, hm)
})
b.Run("sync_map", func(b *testing.B) {
hm := CreateSyncMap()
benchmarkMap(b, hm)
})
b.Run("concurrent map", func(b *testing.B) {
hm := CreateConcurrentMap(100)
benchmarkMap(b, hm)
})
}
结果如下:
NumOfReader = 100
NumOfWriter = 10000
NumOfReader = 10000
NumOfWriter = 10000
NumOfReader = 10000
NumOfWriter = 100
小结:读比写多的时候,sync.map性能最好。写多的时候concurrent map性能较好。