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

-8GOMAXPROCS,默认等于 CPU 核数

ConcurrentMap用例执行一次,花费6.44s

SyncMap用例执行一次,花费13.55s

因此可以得出一个结论:concurrentHashMap在写多读少的情况下,其性能是远优于官方的sync.map的。

项目地址:https://github.com/hfdpx/concurrment-hash-map 欢迎star

posted @ 2022-02-12 10:39  西*风  阅读(1099)  评论(0编辑  收藏  举报