Golang sync.Map底层实现场景示例详解

 更新时间:2023年09月07日 09:50:04   作者:EricLee  
这篇文章主要为大家介绍了Golang sync.Map底层实现及使用场景示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

Go中普通的map是非线程安全的,想要线程安全的访问一个map,有两种方式一种是map+mutex另一种就是原生的sync.Map,这篇文章会详细的介绍sync.Map底层是如何实现的,以及一些常用的场景。

如何保证线程安全?

sync.Map的数据结构如下:

type Map struct {
   mu Mutex // 锁,保证写操作及dirty晋升为read的线程安全
   read atomic.Value // readOnly 只读map
   dirty map[any]*entry // 脏map,当内部有数据时就一定包含read中的数据
   misses int // read未命中次数,当达到一定次数时会触发dirty中的护具晋升到read
}

如果只看这个结构我们可能会有以下几个疑问:

  • sync.Map中也用了mutex那和map+mutex的实现方式不就一样了吗?
  • misses做什么用的?
  • read的类型是一个atomic.Value而dirty是map[any]*entry,为什么不同?

sync.Map中也用了mutex那和map+mutex的实现方式不就一样了吗?

  • 在本质上都是通过map+mutex的实现方式来实现的
  • sync.Map通过增加read map,降低在进行读取操作时的加锁概率,增加读取的性能。

misses做什么用的?

  • misses是用于标记read中未命中次数的
  • 当misses达到一定值时会触发dirty的晋升(晋升为read)

具体源码如下:

// 当执行Load操作时没能在read中命中key,则进行一次miss记录
func (m *Map) missLocked() {
   // 1.miss计数加1
   m.misses++
   // 2.判断dirty是否满足晋升条件
   if m.misses < len(m.dirty) {
      // 2.1不满足直接返回
      return
   }
   // 3.将dirty中的数据转存到read的m中,旧的read中的数据被抛弃
   m.read.Store(readOnly{m: m.dirty})
   // 4.清空dirty
   m.dirty = nil
   // 5.重置miss计数
   m.misses = 0
}

从代码中可以看到:

  • 当misses值大于等于dirty中数据个数的时候会触发dirty的晋升
  • 在dirty晋升时,直直接把read重置成了一个新生成的readOnly,其中m为新的dirty,amended为默认值false,保证每次触发晋升都自动将amended设置为了false
  • 在dirty晋升时,并没有触发数据的拷贝

read的类型是一个atomic.Value而dirty是map[any]*entry,为什么不同?

type readOnly struct {
   m       map[any]*entry // read map中的数据
   amended bool // 标记dirty map中是否有read中没有的key,如果有,则此值为true
}
type entry struct {
   p unsafe.Pointer // *interface{}  一个指向具体数据的指针
}
  • read的类型底层是存储的readOnly类型,而readOnly类型只是在map[any]*entry的基础上增加了一个amended标记
  • 如果amended为false,则代表dirty中没有read中没有的数据,此时可以避免一次dirty操作(会加锁),从而降低无意义的加锁。
  • read被声明为atomic.Value类型是为了满足在无锁的情况下多个Goroutine同时读取read时的数据一致性

sync.Map适用于那些场景?

sync.Map更适合读多写少的场景,而当map需要频繁写入的时候,map+mutex的方案通过控制锁的力度可以达到比sync.Map更好的性能。

sync.Map不支持遍历操作,因为读写分离的设计使得在遍历过程中可能存在一些未完成的修改操作,导致遍历结果不确定。

为什么sync.Map适合读多写少的场景?

sync.Map的读取方法为Load方法,具体的源码实现如下:

func (m *Map) Load(key any) (value any, ok bool) {
   // 1.将read中的数据强转为readOnly
   read, _ := m.read.Load().(readOnly)
   // 2.从read中查询key,检查数据是否存在
   e, ok := read.m[key]
   // 3.如果read中不存在,且amended标记显示dirty中存在read中没有的key,则去dirty中查询
   if !ok && read.amended {
      // 3.1开始操作dirty,需要加锁保证线程安全
      m.mu.Lock()
      // 3.2 重新从read中检查一次,避免在Lock执行前dirty中的数据触发了晋升到read的操作
      read, _ = m.read.Load().(readOnly)
      e, ok = read.m[key]
      // 3.3 同3
      if !ok && read.amended {
         // 3.4 从dirty中查询
         e, ok = m.dirty[key]
         // 3.5 无论是否从dirty中查询到数据,都相当于从read中miss了,需要更新miss计数(更新计数可能会触发dirty数据的晋升)
         m.missLocked()
      }
      // 3.5 操作完成解锁
      m.mu.Unlock()
   }
   // 4.检测结果,ok为false代表没有查询到数据
   // ok为true分为两种情况:1.从read中查询到了数据,read命中;2.从dirty中查询到了数据
   // ok为false分为两种情况:
   //    1.read没有命中,但read.amended为false
   //    2.read没有命中,read.amended为true,但dirty中也不存在
   if !ok {
      return nil, false
   }
   // 返回查询到的数据(这个值也并不一定是真的存在,需要根据p确定是一个正常的值还是一个nil)
   return e.load()
}
// 当从read或者dirty中获取到一个key的值的指针时,需要去加载对应指针的值
func (e *entry) load() (value any, ok bool) {    
   // 1.院子操作获取对应地址的值
   p := atomic.LoadPointer(&e.p)
   // 2.如果值已经不存在或者标记为被删除则返回nil,false
   if p == nil || p == expunged {
      return nil, false
   }
   // 3.返回具体的值,true
   return *(*any)(p), true
}

每次读取数据时,优先从read中读取,且read中数据的读取不需要进行加锁操作;当read中未命中且amended标记显示dirty中存在read中没有的数据时,才进行dirty查询,并加锁。在读多写少的情况下,大多数时候数据都在read中所以可以避免加锁,以此来提高并发读的性能。

sync.Map的写操作方法为Store方法

func (m *Map) Store(key, value any) {
   // 1.从read中查询数据是否已经存在,如果存在则尝试修改
   read, _ := m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok && e.tryStore(&value) {
      // 1.1 read中存在数据,且更新完成,直接返回
      return
   }
   //2.read中没有,准备操作dirty,为保证线程安全加锁
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok {
   // 3.如果在read中查询到数据,则检查是否已经被标记为删除,
      if e.unexpungeLocked() {
      // 3.1 如果被标记为删除需要清空标记并加入到dirty中
         m.dirty[key] = e
      }
      // 3.2 更新entry的值
      e.storeLocked(&value)
   } else if e, ok := m.dirty[key]; ok {
      // 4 如果存在于dirty中,则直接更新entry的值
      e.storeLocked(&value)
   } else {
   // 5 如果之前这个key不存在,则将新的key-value加入到dirty中
      if !read.amended {
      // 5.1 如果read.amended标记显示之前dirty中不存在read中没有的key,则重置dirty,并标amended为true
         // 5.1.1 将read中所有未被标记为删除的entry重新加入到dirty中
         m.dirtyLocked()
         // 5.1.2 更新read.amended标记
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      // 5.2 将新的key-value加入到dirty中
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
}
// 判断entry是否被标记为删除,如果是将其修改为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
   return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

写操作分为以下几种情况:

  • 数据在read中
  • 数据在read中但已经被标记为删除
  • 数据在dirty中
  • 一个全新的数据

当数据在read中时,Store会尝试通过原子操作修改数据,如果原子操作成功,则相当于数据更新完成;

具体的代码如下:

func (e *entry) tryStore(i *any) bool {
   for {
       // 1.获取entry具体的值
      p := atomic.LoadPointer(&e.p)
      // 2.如果数据已经被标记删除,则返回false
      if p == expunged {
         return false
      }
      // 3.更新当前值,并返回true
      if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
         return true
      }
   }
}

当数据在read中已经被标记为删除,此时需要重新将entry加入到dirty中,并更新值(这里本质上增加了一个新的entry只是服用了之前entry的地址空间)

当数据在dirty中时,则直接通过原子操作更新entry的指针;

当数据是一个新数据时,会创建一个新的entry加入到dirty中,并且如果是dirty中的第一个数据则会执行dirtyLocked方法,将read中当前的数据(未标记删除的)加入到dirty中。

dirtyLocked的具体实现如下:

func (m *Map) dirtyLocked() {
    //1 dirty之前是nil的情况才可以进行重置操作
   if m.dirty != nil {
      return
   }
   // 2 获取read中的数据
   read, _ := m.read.Load().(readOnly)
   // 3 初始化dirty
   m.dirty = make(map[any]*entry, len(read.m))
   // 4 遍历read
   for k, e := range read.m {
      // 4.1 将非nil且未被标记为删除的对象加入到dirty中
      if !e.tryExpungeLocked() {
         m.dirty[k] = e
      }
   }
}
// 判断entry是否被标记为删除,如果entry的值为nil,则将其标记为删除
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    // 1 获取entry的值
   p := atomic.LoadPointer(&e.p)
   for p == nil {
       // 2 如果当前entry的值为空,则尝试将此key标记为删除
      if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
         return true
      }
      p = atomic.LoadPointer(&e.p)
   }
   // 3 判断p是否为被标记为删除
   return p == expunged
}

通过上面的分析我们可以发现当写操作频繁时存在以下几个问题

  • dirty中存在大量数据,而read的查询会大概率无法命中,从而导致查询需要查询read和dirty两个map且有额外的冗余操作,所以读性能被大大降低
  • 频繁的无法命中导致dirty数据的晋升,虽然晋升时只是进行指针切换及dirty的清空,但每次晋升后的第一次写入都会导致dirty对read进行拷贝,大大降低性能。
  • 每次写操作为了因为不确定数据是在read还是dirty或者新数据需要进行额外的检查和操作
  • dirty中和read中在某些情况下存在数据重复,内存占用会高一些

综上,在写操作比较频繁的时候,sync.Map的各方面性能都大大降低;而对于一些只有极少写操作的数据(比如:只在服务器启动时加载一次的表格数据),sync.Map可以提高并发操作的性能。

如何删除数据

在上面的dirtyLoacked方法中我们看到当初始化dirty后,会遍历read中的数据,将非nil且未被标记为删除的对象加入到dirty中。由此可以看出read中的数据在删除时并不会立刻删除只是将对象标记为nil或者expunged。

具体代码如下:(Delete方法本质上就是执行的LoadAndDelete)

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
   read, _ := m.read.Load().(readOnly)
   // 1 从read中查询数据
   e, ok := read.m[key]
   // 2 如果read中不存在,且amended标记显示dirty中存在read中没有的key,则去dirty中查询
   if !ok && read.amended {
      // 3.1开始操作dirty,需要加锁保证线程安全
      m.mu.Lock()
      read, _ = m.read.Load().(readOnly)
      // 3.2 重新从read中检查一次,避免在Lock执行前dirty中的数据触发了晋升到read的操作
      e, ok = read.m[key]
      if !ok && read.amended {
         // 3.3 从dirty中查询
         e, ok = m.dirty[key]
         // 3.4 从dirty中删除
         delete(m.dirty, key)
         // 3.5 无论是否从dirty中查询到数据,都相当于从read中miss了,需要更新miss计数(满足条件后会触发dirty晋升)
         m.missLocked()
      }
      m.mu.Unlock()
   }
   // 4 和load一样,这里ok为true可能是在read中读取到数据或者dirty中读取到数据,
   // dirty中的话虽然已经删除但需要清空entry中的指针p
   if ok {
      // 5 标记删除
      return e.delete()
   }
   return nil, false
}

如上述代码第5部分所示,无论entry存在哪里,最终都需要将entry标记为删除。如果存在read中会在dirty初始化时不被加入到dirty中,当dirty再次晋升时read中的数据也就被抛弃了。如果存在dirty中则直接清空了数据并标记entry被删除。

sync.Map的Range方法

sync.Map并不支持遍历,但却提供了一个Range方法,此方法并不是和range关键字一样对map的遍历。

Range方法的具体作用:

  • 遍历所有read中的元素,对其中的每个元素执行函数f
  • 如果当任何一个元素作为参数执行函数f返回false,则立刻中断遍历

虽然在执行初始阶段Range会将dirty的数据晋升一次,但仍然不能保证在执行过程中没有新的数据,所以Range只是遍历了最新的read中的数据,而非全部数据。

// 遍历sync.Map
func (m *Map) Range(f func(key, value any) bool) {
   read, _ := m.read.Load().(readOnly)
   // 1 如果存在未晋升的数据,则先进行一次dirty数据晋升
   if read.amended {
      m.mu.Lock()
      read, _ = m.read.Load().(readOnly)
      if read.amended {
         read = readOnly{m: m.dirty}
         m.read.Store(read)
         m.dirty = nil
         m.misses = 0
      }
      m.mu.Unlock()
   }
   // 2 遍历read中的所有entry,分别执行f函数
   for k, e := range read.m {
      v, ok := e.load()
      if !ok {
         continue
      }
      // 3 当某个entery执行f函数返回false,则中断遍历
      if !f(k, v) {
         break
      }
   }
}

其他

问:什么时候清除被标记删除的value

答:当首次向dirty中存入数据时,会触发dirty复制read中的内容,此时再复制时只复制了非nil且未被标记删除的entry,当dirty再次晋升时就覆盖掉了read中的数据,实现被标记删除的entry的删除。

以上就是Golang sync.Map底层实现场景示例详解的详细内容,更多关于Golang sync.Map底层实现的资料请关注脚本之家其它相关文章!

相关文章

  • go mod 依赖管理的具体使用

    go mod 依赖管理的具体使用

    在Go语言开发中,依赖管理是一项非常重要的工作,Go mod作为官方的包管理工具已经成为了Go语言依赖管理的首选方式,本文就来介绍一下go mod 依赖管理的具体使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • Go之集合slice的实现

    Go之集合slice的实现

    本文主要介绍了Go之集合slice的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • golang高性能的http请求 fasthttp详解

    golang高性能的http请求 fasthttp详解

    fasthttp 是 Go 的快速 HTTP 实现,当前在 1M 并发的生产环境使用非常成功,可以从单个服务器进行 100K qps 的持续连接,总而言之,fasthttp 比 net/http 快 10 倍,下面通过本文给大家介绍golang fasthttp http请求的相关知识,一起看看吧
    2021-09-09
  • go语言反射的基础教程示例

    go语言反射的基础教程示例

    这篇文章主要为大家介绍了go语言反射的基础教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Go单元测试对GORM进行Mock测试

    Go单元测试对GORM进行Mock测试

    这篇文章主要为大家介绍了Go单元测试对GORM进行Mock测试用例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • 使用Go实现优雅重启服务功能

    使用Go实现优雅重启服务功能

    这篇文章主要介绍了如何使用Go来实现优雅重启服务,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • 使用Gomock进行单元测试的方法示例

    使用Gomock进行单元测试的方法示例

    这篇文章主要介绍了使用Gomock进行单元测试的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • Go处理PDF的实现代码

    Go处理PDF的实现代码

    这篇文章主要介绍了Go处理PDF的实现代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • Golang实现Trie(前缀树)的示例

    Golang实现Trie(前缀树)的示例

    本文主要介绍了Golang实现Trie(前缀树)的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • Go 语言中的 http.FileSystem详细解析

    Go 语言中的 http.FileSystem详细解析

    在本文中,我们深入探讨了 Go 语言中的 http.FileSystem 接口,并介绍了它的基本原理、使用方法以及实际应用场景,感兴趣的朋友跟随小编一起看看吧
    2024-03-03

最新评论