当前位置:   article > 正文

golang并发安全-sync.map_golang sync.map如何解决并发问题

golang sync.map如何解决并发问题

sync.map解决的问题

golang 原生map是存在并发读写的问题,在并发读写时候会抛出异常

  1. func main() {
  2. mT := make(map[int]int)
  3. g1 := []int{1, 2, 3, 4, 5, 6}
  4. g2 := []int{4, 5, 6, 7, 8, 9}
  5. go func() {
  6. for i := range g1 {
  7. mT[i] = i
  8. }
  9. }()
  10. go func() {
  11. for i := range g2 {
  12. mT[i] = i
  13. }
  14. }()
  15. time.Sleep(3 * time.Second)
  16. }

抛出异常 fatal error: concurrent map writes

如果将map换成sync.map 那么就不会出现这个问题,下面就简单说说syn.map怎么实现的

基本结构

Map结构体

  1. // Map类型针对两种常见的用例进行了优化:1-当给定键的条目只写一次但读多次时,如在只增长的缓存中,2-当多个goroutine读取、写入和覆盖不相交的键集的条目时。在这两种情况下,与单独的Mutex或RWMutex配对的Go映射相比,使用Map可以显著减少锁争用。
  2. type Map struct {
  3. // 互斥锁mu,操作dirty需先获取mu
  4. mu Mutex
  5. // read是只读的数据结构,可安全并发访问部分,访问它无须加锁,sync.map的所有操作都优先读read
  6. // read中存储结构体readOnly,readOnly中存着真实数据,储存数据时候需要加锁
  7. // read中可能会存在脏数据:即entry被标记为已删除
  8. read atomic.Value // readOnly
  9. // dirty是可以同时读写的数据结构,访问它要加锁,新添加的key都会先放到dirty中
  10. // dirty == nil的情况:
  11. // 1.被初始化
  12. // 2.提升为read后,但它不能一直为nil,否则read和dirty会数据不一致。
  13. // 当有新key来时,会用read中的数据(不是read中的全部数据,而是未被标记为已删除的数据,)填充dirty
  14. // dirty != nil时它存着sync.map的全部数据(包括read中未被标记为已删除的数据和新来的数据)
  15. dirty map[interface{}]*entry
  16. // 统计访问read没有未命中然后穿透访问dirty的次数
  17. // 若miss等于dirty的长度,dirty会提升成read,提升后可以增加read的命中率,减少加锁访问dirty的次数
  18. misses int
  19. }

 readOnly结构体

  1. //第一点的结构read存的就是readOnly
  2. type readOnly struct {
  3. m map[any]*entry //m是一个map,key是interface,value是指针entry,其指向真实数据的地址,
  4. amended bool // amended等于true代表dirty中有readOnly.m中不存在的entry。
  5. }

entry结构体

  1. type entry struct {
  2. // p:
  3. // expunged: 删除; nil: 逻辑删除但存在dirty; 数据
  4. p unsafe.Pointer // *interface{}
  5. }

Load方法

代码解说

Load:读取数据

  1. // Load 返回 map 中key 对应的值,如果没有值,则返回 nil。
  2. // ok 结果表示是否在 map 中找到了 value。
  3. func (m *Map) Load(key any) (value any, ok bool) {
  4. read, _ := m.read.Load().(readOnly) // 从read 读取数据,并转换readonly
  5. e, ok := read.m[key]
  6. if !ok && read.amended { // readonly没有找到对应数据
  7. m.mu.Lock()
  8. // 双重检测:
  9. // 再检查一次readonly,以防中间有Map.dirty被替换为readonly
  10. read, _ = m.read.Load().(readOnly)
  11. e, ok = read.m[key]
  12. if !ok && read.amended { // 去 dirty查找对应数据
  13. e, ok = m.dirty[key]
  14. // 无论Map.dirty中是否有这个key,miss都加一,
  15. // 若miss大小等于dirty的长度,dirty中的元素会被加到Map.read中
  16. m.missLocked()
  17. }
  18. m.mu.Unlock()
  19. }
  20. if !ok {
  21. return nil, false
  22. }
  23. return e.load()// 若entry.p被删除(等于nil或expunged)返回nil和不存在(false),否则返回对应的值和存在(true)
  24. }

missLocked:dirty是如何提升为read

  1. func (m *Map) missLocked() {
  2. m.misses++ // 每次misses+1
  3. if m.misses < len(m.dirty) {
  4. return
  5. }
  6. // 当misses等于dirty的长度,m.dirty转换readOnly,amended被默认赋值成false
  7. m.read.Store(readOnly{m: m.dirty})
  8. m.dirty = nil
  9. m.misses = 0
  10. }

流程图

 load: 会先从readOnly查找数据, 如果没有开启加锁,再次访问readOnly, 再次没有再去dirty去查。

Store方法

代码解说

store: 赋值

  1. // Store 设置key value
  2. func (m *Map) Store(key, value any) {
  3. read, _ := m.read.Load().(readOnly) // 转换readOnly
  4. // 若key在readOnly.m中且 e.tryStore 不为 false(没有逻辑删除)
  5. if e, ok := read.m[key]; ok && e.tryStore(&value) {
  6. return
  7. }
  8. m.mu.Lock()
  9. // 双重检测:
  10. // 再检查一次readonly,以防中间有Map.dirty被替换为readonly
  11. read, _ = m.read.Load().(readOnly)
  12. if e, ok := read.m[key]; ok {
  13. // entry.p状态是expunged置为nil
  14. // 如果是逻辑删除就需要清除标记了
  15. if e.unexpungeLocked() {
  16. // 之前dirty中没有此key,所以往dirty中添加此key
  17. m.dirty[key] = e
  18. }
  19. // cas: 赋值
  20. e.storeLocked(&value)
  21. } else if e, ok := m.dirty[key]; ok {
  22. e.storeLocked(&value)
  23. } else {
  24. // dirty中没有新数据,往dirty中添加第一个新key
  25. if !read.amended {
  26. // 把readOnly中未标记为删除的数据拷贝到dirty中
  27. m.dirtyLocked()
  28. // amended:true,现在dirty有readOnly中没有的key
  29. m.read.Store(readOnly{m: read.m, amended: true})
  30. }
  31. m.dirty[key] = newEntry(value)
  32. }
  33. m.mu.Unlock()
  34. }

tryStore:尝试写入数据

  1. func (e *entry) tryStore(i *any) bool {
  2. for {
  3. p := atomic.LoadPointer(&e.p)
  4. if p == expunged { // 如果逻辑删除就返回false
  5. return false
  6. }
  7. // 不是就将value写入
  8. if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
  9. return true
  10. }
  11. }
  12. }

dirtyLocked: 将readOnly 未删除的放到dirty

  1. func (m *Map) dirtyLocked() {
  2. if m.dirty != nil {
  3. return
  4. }
  5. // dirty为nil时,把readOnly中没被标记成删除的entry添加到dirty
  6. read, _ := m.read.Load().(readOnly)
  7. m.dirty = make(map[interface{}]*entry, len(read.m))
  8. for k, e := range read.m {
  9. // tryExpungeLocked函数在entry未被删除时返回false,反之返回true
  10. if !e.tryExpungeLocked() { // entry没被删除
  11. m.dirty[k] = e
  12. }
  13. }
  14. }

流程图

sync.map不适合用于频繁插入新key-value的场景,因为此操作会频繁加锁访问dirty会导致性能下降。更新操作在key存在于readOnly中且值没有被标记为删除(expunged)的场景下会用无锁操作CAS进行性能优化,否则也会加锁访问dirty。

Delete方法

代码解说

LoadAndDelete:查找删除

  1. func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
  2. read, _ := m.read.Load().(readOnly)
  3. e, ok := read.m[key]
  4. if !ok && read.amended { // readOnly不存在此key,但dirty中可能存在
  5. // 加锁访问dirty
  6. m.mu.Lock()
  7. // 双重检测
  8. read, _ = m.read.Load().(readOnly)
  9. e, ok = read.m[key]
  10. // readOnly不存在此key,但是dirty中可能存在
  11. if !ok && read.amended {
  12. e, ok = m.dirty[key]
  13. delete(m.dirty, key)
  14. m.missLocked() // 判断dirty是否可以转换readOnly,可以就转换
  15. }
  16. m.mu.Unlock()
  17. }
  18. if ok {
  19. // 如果entry.p不为nil或者expunged,则把逻辑删除(标记为nil)
  20. return e.delete()
  21. }
  22. return nil, false
  23. }

delete:逻辑删除

  1. func (e *entry) delete() (value any, ok bool) {
  2. for {
  3. p := atomic.LoadPointer(&e.p)
  4. if p == nil || p == expunged { // 已经处理或者不存在
  5. return nil, false
  6. }
  7. if atomic.CompareAndSwapPointer(&e.p, p, nil) { // 逻辑删除
  8. return *(*any)(p), true
  9. }
  10. }
  11. }

流程图

Range方法

代码解说

Range:轮训元素

  1. func (m *Map) Range(f func(key, value any) bool) {
  2. read, _ := m.read.Load().(readOnly)
  3. if read.amended { // 如果dirty存在数据
  4. m.mu.Lock()
  5. // 双重检测
  6. read, _ = m.read.Load().(readOnly)
  7. if read.amended {
  8. // readOnly.amended被默认赋值成false
  9. read = readOnly{m: m.dirty}
  10. m.read.Store(read)
  11. m.dirty = nil
  12. m.misses = 0
  13. }
  14. m.mu.Unlock()
  15. }
  16. // 遍历readOnly.m
  17. for k, e := range read.m {
  18. v, ok := e.load()
  19. if !ok {
  20. continue
  21. }
  22. if !f(k, v) {
  23. break
  24. }
  25. }
  26. }

流程图 

Range:全部key都存在于readOnly中时,是无锁遍历的,性能最优。如果readOnly只存在Map中的部分key时,会一次性加锁拷贝dirty的元素到readOnly,减少多次加锁访问dirty中的数据。

总结

1- sync.map 结构体加了readOnly 和 dirty 来实现读写分离,load,store, delete,range 每次都会优先访问read,后面访问dirty都会双重检测以防加锁前Map.dirty可能已被提升为read

2- sync.map不适合写多读少,从store 代码中可以看出会频繁加锁访问dirty,双重检测等等,这些都会导致性能下降

3- sync.map 没有提供对read, dirty 的长度方法,这个对象使用在于并发场景下,会额外带来锁竞争的问题

4- misses 是 统计访问read没有未命中然后穿透访问dirty的次数 ,如果等于dirty会转换readOnly

5- entry 有三种类型 expunged: 删除; nil: 逻辑删除但存在dirty; 数据 。其中expunged 会在 unexpungeLocked 方法中进行赋值(在store时候会加锁访问dirty,把readOnly中的未被标记为删除的所有entry指针放到dirty,之前被delete方法标记为删除状态的entry=nil都变为expunged,那这些被标记为expunged的entry将不会出现在dirty中。)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/850348
推荐阅读
相关标签
  

闽ICP备14008679号