Golang-分段锁实现并发安全的Map
Golang - 分段锁实现并发安全Map
一.引言
我们一般有两种方式来降低锁的竞争:
- 第一种:减少锁的持有时间,sync.Map即是采用这种策略,通过冗余的数据结构,使得需要持有锁的时间,大大减少。
- 第二种:降低锁的请求频率,锁分解和锁分段技术即是这种思想的体现。
锁分段技术又可称为分段锁机制
什么叫做分段锁机制?
将数据分为一段一段的存储,然后给每一段数据配备一把锁.
这样在多线程情况下,不同线程操作不同段的数据不会造成冲突,线程之间也不会存在锁竞争,有效的提高了并发访问的效率
二.实现原理
首先我们需要给数据进行分段,属于同一个段的数据放在一起,我们采用golang原生的map来充当段的容器,用于存储元素,将key通过哈希映射的形式分配到不同的段中。
// SharedMap 并发安全的小map,ShardCount 个这样的小map数组组成一个大map
type SharedMap struct {
items map[string]interface{}
sync.RWMutex // 读写锁,保护items
}
可以看到,我们给每个段都配备了一个内置的读写锁,用于保护段内的数据安全
// ShardCount 底层小shareMap数量
var ShardCount = 32
// ConcurrentHashMap 并发安全的大map,由 ShardCount 个小mao数组组成,方便实现分段锁机制
type ConcurrentHashMap []*SharedMap
然后 ShardCount 个ShareMap就组成了一个大的并发安全的map。
其哈希函数采用了著名的fnv函数
// fnv32 hash函数
func fnv32(key string) uint32 {
// 著名的fnv哈希函数,由 Glenn Fowler、Landon Curt Noll和 Kiem-Phong Vo 创建
hash := uint32(2166136261)
const prime32 = uint32(16777619)
keyLength := len(key)
for i := 0; i < keyLength; i++ {
hash *= prime32
hash ^= uint32(key[i])
}
return hash
}
1.New
// New 创建一个新的concurrent map.
func New() ConcurrentHashMap {
m := make(ConcurrentHashMap, ShardCount)
for i := 0; i < ShardCount; i++ {
m[i] = &SharedMap{items: make(map[string]interface{})}
}
return m
}
直接采用make初始化指定数量个ShareMap,并采用数组的形式保证这些初始化好的ShareMap
2.Set/Get/Delete/Has
// GetShardMap 返回给定key的sharedMap
func (m ConcurrentHashMap) GetShardMap(key string) *SharedMap {
return m[uint(fnv32(key))%uint(ShardCount)]
}
// Set 添加 key-value
func (m ConcurrentHashMap) Set(key string, value interface{}) {
// Get map shard.
shard := m.GetShardMap(key)
shard.Lock()
shard.items[key] = value
shard.Unlock()
}
// Get 返回指定key的value值
func (m ConcurrentHashMap) Get(key string) (interface{}, bool) {
shard := m.GetShardMap(key)
shard.RLock()
val, ok := shard.items[key]
shard.RUnlock()
return val, ok
}
// Remove 删除一个元素
func (m ConcurrentHashMap) Remove(key string) {
// Try to get shard.
shard := m.GetShardMap(key)
shard.Lock()
delete(shard.items, key)
shard.Unlock()
}
// Has 判断元素是否存在
func (m ConcurrentHashMap) Has(key string) bool {
// Get shard
shard := m.GetShardMap(key)
shard.RLock()
// See if element is within shard.
_, ok := shard.items[key]
shard.RUnlock()
return ok
}
都是先将key通过hash函数确定和获取其所属的ShareMap,然后锁住该段,直接操作数据。
3.Count/Keys
// Count 统计元素总数
func (m ConcurrentHashMap) Count() int {
count := 0
for i := 0; i < ShardCount; i++ {
shard := m[i]
shard.RLock()
count += len(shard.items)
shard.RUnlock()
}
return count
}
遍历所有的ShareMap,逐个统计,注意,遍历的时候每个ShareMap时,都需要加锁
// Keys 以字符串数组的形式返回所有key
func (m ConcurrentHashMap) Keys() []string {
count := m.Count()
ch := make(chan string, count)
go func() {
// Foreach shard.
wg := sync.WaitGroup{}
wg.Add(ShardCount)
for _, shard := range m {
go func(shard *SharedMap) {
// Foreach key, value pair.
shard.RLock()
for key := range shard.items {
ch <- key
}
shard.RUnlock()
wg.Done()
}(shard)
}
wg.Wait()
close(ch)
}()
// Generate keys
keys := make([]string, 0, count)
for k := range ch {
keys = append(keys, k)
}
return keys
}
每一个段都启动一个goroutine,往缓冲通道ch中塞入key,采用WaitGroup的方式等所有key都被塞入ch后,外部goroutine持续从ch中读取key放入keys数组中,然后直接返回keys。
三.性能比较
对比官方的sync.Map
package main
import (
"fmt"
"github.com/hfdpx/concurrment-hash-map"
"strconv"
"sync"
"time"
)
func main() {
count:=10000000
loop:=5
startT := time.Now()
cmap := concurrent_hash_map.New()
for i:=0;i<count;i++{
cmap.Set(strconv.Itoa(i), strconv.Itoa(i))
}
fmt.Printf("cmap 写 time cost = %v\n", time.Since(startT))
startT = time.Now()
var m sync.Map
for i:=0;i<count;i++{
m.Store(strconv.Itoa(i), strconv.Itoa(i))
}
fmt.Printf("sync.map 写 time cost = %v\n", time.Since(startT))
startT = time.Now()
for j:=0;j<loop;j++{
for i:=0;i<count;i++{
cmap.Get(strconv.Itoa(i))
}
}
fmt.Printf("cmap 读 time cost = %v\n", time.Since(startT))
startT = time.Now()
for j:=0;j<loop;j++{
for i:=0;i<count;i++{
m.Load(strconv.Itoa(i))
}
}
fmt.Printf("sync.map 读 time cost = %v\n", time.Since(startT))
startT = time.Now()
for i:=count;i<count*loop;i++{
cmap.Set(strconv.Itoa(i), strconv.Itoa(i))
}
fmt.Printf("cmap 写 time cost = %v\n", time.Since(startT))
startT = time.Now()
for i:=count;i<count*loop;i++{
m.Store(strconv.Itoa(i), strconv.Itoa(i))
}
fmt.Printf("sync.map 写 time cost = %v\n", time.Since(startT))
}
我们先分别向cmap和sync.map写入一百万数据,然后多次读取这一百万数据,最后再次写入四百万数据,其结果如下:
cmap 写 time cost = 484.604625ms
sync.map 写 time cost = 890.503084ms
cmap 读 time cost = 1.0355345s
sync.map 读 time cost = 1.189747875s
cmap 写 time cost = 2.158350792s
sync.map 写 time cost = 4.709973666s
在首次写入一百万数据时,cmap耗时484ms,而sync.map耗时890ms,并且在最后写入四百万数据时,cmp仅耗时2.16s,而sync.map耗时4.71s,sync耗时超过cpm耗时一倍有余。
我们再用golang官方的性能基准测试验证一下
package concurrent_hash_map
import (
"math/rand"
"strconv"
"sync"
"testing"
)
// 1000万次的赋值,1000万次的读取
var times int = 10000000
// BenchmarkTestConcurrentMap 测试ConcurrentMap
func BenchmarkTestConcurrentMap(b *testing.B) {
for k := 0; k < b.N; k++ {
b.StopTimer()
// 产生10000个不重复的键值对(string -> int)
testKV := map[string]int{}
for i := 0; i < 10000; i++ {
testKV[strconv.Itoa(i)] = i
}
// 新建一个ConcurrentMap
pMap := New()
// set到map中
for k, v := range testKV {
pMap.Set(k, v)
}
// 开始计时
b.StartTimer()
wg := sync.WaitGroup{}
wg.Add(2)
// 赋值
go func() {
// 对随机key,赋值times次
for i := 0; i < times; i++ {
index := rand.Intn(times)
pMap.Set(strconv.Itoa(index), index+1)
}
wg.Done()
}()
// 读取
go func() {
// 对随机key,读取times次
for i := 0; i < times; i++ {
index := rand.Intn(times)
pMap.Get(strconv.Itoa(index))
}
wg.Done()
}()
// 等待两个协程处理完毕
wg.Wait()
}
}
// BenchmarkTestSyncMap 测试sync.map
func BenchmarkTestSyncMap(b *testing.B) {
for k := 0; k < b.N; k++ {
b.StopTimer()
// 产生10000个不重复的键值对(string -> int)
testKV := map[string]int{}
for i := 0; i < 10000; i++ {
testKV[strconv.Itoa(i)] = i
}
// 新建一个sync.Map
pMap := &sync.Map{}
// set到map中
for k, v := range testKV {
pMap.Store(k, v)
}
// 开始计时
b.StartTimer()
wg := sync.WaitGroup{}
wg.Add(2)
// 赋值
go func() {
// 对随机key,赋值
for i := 0; i < times; i++ {
index := rand.Intn(times)
pMap.Store(strconv.Itoa(index), index+1)
}
wg.Done()
}()
// 读取
go func() {
// 对随机key,读取10万次
for i := 0; i < times; i++ {
index := rand.Intn(times)
pMap.Load(strconv.Itoa(index))
}
wg.Done()
}()
// 等待两个协程处理完毕
wg.Wait()
}
}
其结果如下:
goos: darwin
goarch: amd64
pkg: concurrent_hash_map
BenchmarkTestConcurrentMap
BenchmarkTestConcurrentMap-8 1 6443625874 ns/op
BenchmarkTestSyncMap
BenchmarkTestSyncMap-8 1 13556847750 ns/op
PASS
-8
即 GOMAXPROCS
,默认等于 CPU 核数
ConcurrentMap用例执行一次,花费6.44s
SyncMap用例执行一次,花费13.55s
因此可以得出一个结论:concurrentHashMap在写多读少的情况下,其性能是远优于官方的sync.map的。
项目地址:https://github.com/hfdpx/concurrment-hash-map 欢迎star