• 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

  • 相关阅读:
    Go 好用第三方库
    Go 的beego 框架
    Go 的gin 框架 和 gorm 和 html/template库
    Go 常用的方法
    Dijkstra 的两种算法
    邻接矩阵
    next permutation 的实现
    最优二叉树 (哈夫曼树) 的构建及编码
    思维题— Count the Sheep
    STL— bitset
  • 原文地址:https://www.cnblogs.com/yinbiao/p/15884420.html
Copyright © 2020-2023  润新知