[Golang]解决Map的并发性问题:sync.Map

先说问题

Golang的原生Map是不支持并发写的,但是可以并发读,是一种非线程安全的结构。以下程序直接报错: fatal error: concurrent map read and map write,即便访问的是不同的key。

func main() {
   m:=make(map[int]int)
   m[1] = 1
   go func() {
      for {
         m[0] = 1
      }
   }()
   go func() {
      for {
         _ = m[1]
      }
   }()
   time.Sleep(time.Second)
}

解决方案

Golang 1.9之后的版本加入了sync.Map结构,一种并发安全的结构,源码中这样介绍

翻译过来就是(1)写一次但是读取多次,只会增加缓存的长度而已(2)多线程读写不同的键能够明显减少锁的争用。
源码解析

1、基础结构

sync.Map结构利用了两个原生map结构读写,read主要负责读,而dirty主要负责写,只需要在合适的时机进行同步即可。此外,read和dirty的map的值存的都是指针,指向都是同一份数据,所以不会有太大的内存消耗,而且修改read[key]那么dirty[key]的值也被修改了。

// sync.Map结构
type Map struct {
   mu Mutex
   // 主要负责读
   read atomic.Value 
   // 可以看成是read的后备箱,主要负责写
   // interface{}是key的类型,*entry是value的类型
   dirty map[interface{}]*entry 
   // 当计数达到len(dirty)时,同步dirty->read,nil->dirty
   // 【坑位1:触发计数条件】
   misses int
}
// Map结构成员read类型atomic.Value结构
type Value struct {
   v interface{}
}
// Value成员v存的便是readOnly结构
type readOnly struct {
    // 和dirty一样的结构,后面我直接说read代替m
   m       map[interface{}]*entry 
   // 如果dirty里面有read没有的[删除的不算]entry则为true,可以理解为dirty不落后于read
   // 毕竟优先往diryt里面写,read先放出去让用户去读
   amended bool 
}
// map中key-value中的value的类型
type entry struct {
        // 可以指向正常的变量;或者等于nil,其实nil也可以看成正成变量,但是还是单独拎出来吧;或者等于expunged,一个用于标识的自定义指针变量
        // expunged标志作用:为了支持延迟删除【坑位2:如何支持】
        // expunged含义:read里面标记了该entry的删除,同时dirty不为nil且dirty中却没有存上该entry,当然key也没有存
        p unsafe.Pointer
}
// 一个用于标识的自定义指针变量
var expunged = unsafe.Pointer(new(interface{}))

2、常规方法:增改、删、查

**增改:**store函数。重点关注对象dirty成员;只要dirty不为nil,store函数执行完必须保证dirty不能落后于read【坑位3:为什么】。扩容只会发生在dirty上,读read不影响。

func (m *Map) Store(key, value interface{}) {
   // 前面说了Map里面的read成员存的就是ReadOnly结构,所以先转换过去
   read, _ := m.read.Load().(readOnly)
   // 拿到了该key对应的old_value,那就尝试把old_value覆盖掉
   if e, ok := read.m[key]; ok && e.tryStore(&value) {
      return
   }
   // read里面没有该key对应的value;或者没存上
   //涉及到了写的操作,需要加锁
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly) // double check
   // read里面有该key对应的value,但是value已经被标记成expunged了(毕竟上面没return)
   if e, ok := read.m[key]; ok {
       // 将read里面标记为expunged的数据更新到dirty中,同时修改标记entry=nil,毕竟dirty里面也会有该数据了
      if e.unexpungeLocked() {
         m.dirty[key] = e
      }
      // 更新该值
      e.storeLocked(&value)
   // read里面没找到,那么本身dirty就是不落后于read的,直接更新了dirty里面该key对应的value即可
   } else if e, ok := m.dirty[key]; ok {
      e.storeLocked(&value)
   // read里面和dirty里面都没有,那就要新增了
   } else {
       // 首先看一下dirty是不是落后于read,落后就进入if
      if !read.amended {
          // 用read去更新dirty,read->dirty
         m.dirtyLocked()
         // 更新一下read,数据没变,只不过dirty不再落后于它了,amended设置为true
         // 可以看出,dirty不为nil的时候,amended一定为true;换句话说,dirty只要不为nil,就一定不落后于read
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      // 最后把该key,value加入到dirty;能看出来read里面是没有的
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
}
func (e *entry) tryStore(i *interface{}) bool {
   for {
      p := atomic.LoadPointer(&e.p)
      // 根据前面说的expunged的含义,同时要保证store函数执行完必须保证dirty不能落后于read
      // 所以不能简单的更新该key对应的值,因为dirty里面还没有这份数据呢
      if p == expunged {
         return false
      }
      // 走正常更新逻辑
      if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
         return true
      }
   }
}
func (e *entry) unexpungeLocked() (wasExpunged bool) {
   return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// 将read里面那些没意义的(值为nil)和标记删除的(值为expunged)过滤掉,之后赋值给dirty
// 同时把没意义的(值为nil)值修改为标记删除(expunged),毕竟diryt里面没有它
// 【这里注意下:nil是可以成为我们的一个普通值的,而expunged是我们自定义标识】
func (m *Map) dirtyLocked() {
   if m.dirty != nil {
      return
   }
   read, _ := m.read.Load().(readOnly)
   m.dirty = make(map[interface{}]*entry, len(read.m))
   //彻底删除还需要达到misses计数,触发missLocked函数
   for k, e := range read.m {
      if !e.tryExpungeLocked() {
         m.dirty[k] = e
      }
   }
}
// 
func (e *entry) tryExpungeLocked() (isExpunged bool) {
   p := atomic.LoadPointer(&e.p)
   for p == nil {
      if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
         return true
      }
      p = atomic.LoadPointer(&e.p)
   }
   return p == expunged
}

删: delete函数。

func (m *Map) Delete(key interface{}) {
   m.LoadAndDelete(key)
}
// 直接看这个就好
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   // read里没找到,而且dirty是不落后于read的
   if !ok && read.amended {
      m.mu.Lock()
      read, _ = m.read.Load().(readOnly) // double check
      e, ok = read.m[key]
      if !ok && read.amended {
         e, ok = m.dirty[key]
         // 直接在dirty里面对这个key删除即可,read根本无感知
         delete(m.dirty, key)
         // 【填坑1】read中找不到,且dirty不落后于read,无论在dirty找没找到都会使集数加1
         m.missLocked()
      }
      m.mu.Unlock()
   }
   if ok {
      return e.delete()
   }
   return nil, false
}
func (e *entry) delete() (value interface{}, ok bool) {
   for {
      p := atomic.LoadPointer(&e.p)
      if p == nil || p == expunged {
         return nil, false
      }
    // 可以看到只有这一个地方会把value设置nil,而store函数是能保证dirty不落后于read的,
      if atomic.CompareAndSwapPointer(&e.p, p, nil) {
         return *(*interface{})(p), true
      }
   }
}
// 当read如果 落后于dirty多次,就会将dirty提升为read,这也是read的唯一来源
func (m *Map) missLocked() {
   m.misses++
   if m.misses < len(m.dirty) {
      return
   }
   m.read.Store(readOnly{m: m.dirty})
   m.dirty = nil
   m.misses = 0
}

查:load函数。可以看出读是不会立刻加锁的

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   // read里没查到,而且dirty是不落后于read的那就从dirty里面再查一下
   if !ok && read.amended {
      m.mu.Lock()
      read, _ = m.read.Load().(readOnly) // double check
      e, ok = read.m[key]
      if !ok && read.amended {
         e, ok = m.dirty[key]
         // 【填坑1】触发条件read里面没找到去不落后于read的dirty里面找就会触发
         m.missLocked()
      }
      m.mu.Unlock()
   }
   if !ok {
      return nil, false
   }
   return e.load()
}

注意的一些点可能会帮助理解:
1、结论1: value是expunged那么dirty一定不为nil。value被标记为expunged的唯一方式就是在store函数dirtyLocked()-> tryExpungeLocked(),同时会将amended设置为true,这两个操作是绑定的。所以只要有value是expunged,那么amended一定为true,也就表示dirty不落后于read的,而且可以看出expunged一定是只会出现在read里面的key对应的value上,dirty是一定不会有的,毕竟这是唯一设置expunged的位置。
2、结论2: dirty只要不为nil,那么dirty就一定不会落后于read。再查看一下dirty被新增或者删除的代码位置【置为空,修改值不算,因为都不会影响dirty是否不落后于read】。(1)store函数中在read中找到了,但是value被标记为expunged,那么在dirty中新增该key,如第一点所说expunged存在就表示dirty不落后于read。(2)store函数中read中没找到且dirty中也没有,那么就会在dirty中新增,这种方式更不会导致dirty落后于read。所以只要dirty不为nil就一定会一直不落后于read的。
3、结论3: value是nil,那么dirty是nil要么非nil(废话),说白了nil就是一个寻常的值而已,就像你可以往map存放(key,nil),在这里作为了中间态(当然,中间态设置为1,2,3…都可以,只要是个对你没有意义的变量就行)。value被标记为nil有两种方式,一种是在store函数中unexpungeLocked()中,而触发这个语句的条件是value为expunged,同时如第一点所说,dirty是不落后于read的;另一种是在delete函数中,在read中找到了该key,那么如果该key对应value非nil且非expunged,那么就会更新为nil,而此时dirty可能为nil或者不为nil,但是不为nil的时候dirty也一定会有该key的,毕竟read都是从dirty中更新过去的,所以对于value正常的read有dirty一定有。
【填坑2:延迟删除】 综上可以看出,在missLocked()中直接用dirty覆盖掉read的时候就会剔除掉了read中的expunged标识的(key,value),毕竟expunged标志只在read中的value才有。
【填坑3:为何不落后】 因为涉及到用dirty更新read的操作,那么只要dirty不为nil就一定要不落后于read才行啊。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值