简单整两句
Go语言内置的map数据结构是线程不安全的,如果存在多个线程同时读或写同一个map时,常常会导致难以预料的问题。
所以,有很多程序员在使用Go语言内置的map时,会在外面包装一层,即封装一层读写接口,并在接口实现内部引入读锁或写锁,以此来保证多线程安全访问map。
在上一篇博客中,我们知道,即使是使用读锁,也会严重降低程序性能。因此针对map的优化,也存在很多方案。
sync.map方案
sync.map是Golang在1.9版本中引入的线程安全的高性能map数据结构,在实现中引入了缓存机制:
程序在读map时,会首先到Read内存区读数据,这个Read内存区是不需要锁机制来访问的,如果Read内存区中不存在想要的值,会到Dirty区域找数据,这个Dirty区域的访问是需要加锁的。
由此可见,sync.map适合在读多写少的情况优化程序性能。如果写入map操作很多,同样会导致各种锁引发的性能问题。
concurrent_map方案
除了sync.map这种通过缓存的方式提升map访问性能的方式外,也存在另一种优化方式,即concurrent_map(Java中有类似的实现)。
sync.map的实现中,对Dirty区的访问使用的锁是锁住整个map。但如果两次写或读是针对同一个map的不同区域的访问,那锁住整个map就显得不那么合理了。
所以,concurrent_map的实现原理大致是这样的:
左图是使用RWLock或sync.map的锁机制,即对整块map内存加锁。而右图是concurrent_map的锁机制,即对map内存细粒度的加锁。这样能够最大限度的减少锁冲突,进而提高性能。
上代码
这里有几段代码,分别通过RWLock、sync.map、concurrent_map三种方式访问map,然后通过测试程序对比不同方案的性能差异。
先看测试代码:(map_benchmark_test.go)
package maps
import (
"strconv"
"sync"
"testing"
)
const (
NumOfReader = 100
NumOfWriter = 10
)
type Map interface {
Set(key interface{}, val interface{})
Get(key interface{}) (interface{}, bool)
Del(key interface{})
}
func benchmarkMap(b *testing.B, hm Map) {
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
for i := 0; i < NumOfWriter; i++ {
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
hm.Set(strconv.Itoa(i), i*i)
hm.Set(strconv.Itoa(i), i*i)
hm.Del(strconv.Itoa(i))
}
wg.Done()
}()
}
for i := 0; i < NumOfReader; i++ {
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
hm.Get(strconv.Itoa(i))
}
wg.Done()
}()
}
wg.Wait()
}
}
func BenchmarkSyncmap(b *testing.B) {
b.Run("map with RWLock", func(b *testing.B) {
hm := CreateRWLockMap()
benchmarkMap(b, hm)
})
b.Run("sync.map", func(b *testing.B) {
hm := CreateSyncMapBenchmarkAdapter()
benchmarkMap(b, hm)
})
b.Run("concurrent map", func(b *testing.B) {
superman := CreateConcurrentMapBenchmarkAdapter(199)
benchmarkMap(b, superman)
})
}
首先定义了一个map接口(type Map interface ),接口函数有Set()、Get()、Del()。
然后封装了一个工具函数benchmarkMap,来针对一个map实例(对象)进行读和写操作。
最后是BenchmarkSyncmap函数,它调用benchmarkMap函数,并传入三种不同的map实现方案实现的map实例进行性能测试。
BenchmarkSyncmap函数中调用的CreateRWLockMap、CreateSyncMapBenchmarkAdapter、CreateConcurrentMapBenchmarkAdapter函数分别存在于另外三个go源码中。
这段测试用例代码还可以通过调整常量的值NumOfReader 、NumOfWriter来控制读多写少还是读少写多。
使用RWLock实现的map代码:(rw_map.go)
package maps
import "sync"
type RWLockMap struct {
m map[interface{}]interface{}
lock sync.RWMutex
}
func (m *RWLockMap) Get(key interface{}) (interface{}, bool) {
m.lock.RLock()
v, ok := m.m[key]
m.lock.RUnlock()
return v, ok
}
func (m *RWLockMap) Set(key interface{}, value interface{}) {
m.lock.Lock()
m.m[key] = value
m.lock.Unlock()
}
func (m *RWLockMap) Del(key interface{}) {
m.lock.Lock()
delete(m.m, key)
m.lock.Unlock()
}
func CreateRWLockMap() *RWLockMap {
m := make(map[interface{}]interface{}, 0)
return &RWLockMap{m: m}
}
代码末尾的CreateRWLockMap函数用来对外提供创建RWLockMap实例服务(在map_benchmark_test.go中已经用到)。
使用sync.map实现的map:(sync_map_benchmark_adapter.go)
package maps
import "sync"
func CreateSyncMapBenchmarkAdapter() *SyncMapBenchmarkAdapter {
return &SyncMapBenchmarkAdapter{}
}
type SyncMapBenchmarkAdapter struct {
m sync.Map
}
func (m *SyncMapBenchmarkAdapter) Set(key interface{}, val interface{}) {
m.m.Store(key, val)
}
func (m *SyncMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
return m.m.Load(key)
}
func (m *SyncMapBenchmarkAdapter) Del(key interface{}) {
m.m.Delete(key)
}
concurrent_map:(concurrent_map_benchmark_adapter.go)
package maps
import "github.com/easierway/concurrent_map"
type ConcurrentMapBenchmarkAdapter struct {
cm *concurrent_map.ConcurrentMap
}
func (m *ConcurrentMapBenchmarkAdapter) Set(key interface{}, value interface{}) {
m.cm.Set(concurrent_map.StrKey(key.(string)), value)
}
func (m *ConcurrentMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
return m.cm.Get(concurrent_map.StrKey(key.(string)))
}
func (m *ConcurrentMapBenchmarkAdapter) Del(key interface{}) {
m.cm.Del(concurrent_map.StrKey(key.(string)))
}
func CreateConcurrentMapBenchmarkAdapter(numOfPartitions int) *ConcurrentMapBenchmarkAdapter {
conMap := concurrent_map.CreateConcurrentMap(numOfPartitions)
return &ConcurrentMapBenchmarkAdapter{conMap}
}
这里使用了github中的开源方案:https://github.com/easierway/concurrent_map
需要先安装一下:
若访问github或Google较慢,可使用加速器:
http://91tianlu.date/aff.php?aff=3468
$ go get github.com/easierway/concurrent_map
在读次数10倍于写时,运行测试用例:
#const (
# NumOfReader = 100
# NumOfWriter = 10
#)
$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4 430 2643960 ns/op
BenchmarkSyncmap/sync.map-4 525 2268836 ns/op
BenchmarkSyncmap/concurrent_map-4 745 1570330 ns/op
PASS
ok map 4.464s
读写次数各占比50%时,运行测试用例:
#const (
# NumOfReader = 100
# NumOfWriter = 100
#)
$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4 48 22778590 ns/op
BenchmarkSyncmap/sync.map-4 48 41247146 ns/op
BenchmarkSyncmap/concurrent_map-4 99 11579204 ns/op
PASS
ok map 5.025s
写次数2倍于读时,运行测试用例:
#const (
# NumOfReader = 100
# NumOfWriter = 200
#)
$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4 48 22778590 ns/op
BenchmarkSyncmap/sync.map-4 48 41247146 ns/op
BenchmarkSyncmap/concurrent_map-4 99 11579204 ns/op
PASS
ok map 5.025s
从测试结果上看,无论是读多写少、读少写多、还是各占50%的情况,concurrent_map的性能都优于前两者。
(原作者录制视频课程中时,在极端的读多写少的情况sync.map性能较优秀,可能是原作者后期对concurrent_map进行了优化)。
注:
博客内容为极客时间视频课《Go语言从入门到实战》学习笔记。
参考课程链接:
https://time.geekbang.org/course/intro/160?code=NHxez89MnqwIfa%2FvqTiTIaYof1kxYhaEs6o2kf3ZxhU%3D&utm_term=SPoster
博客参考代码:
https://github.com/geektime-geekbang/go_learning/tree/master/code/ch48/maps
若访问github或Google较慢,可使用加速器:
http://91tianlu.date/aff.php?aff=3468