ninja_ken  

map是golang的内置数据类型之一,日常工作中用起来真的是非常方便。可它也有个明显的不足之处,就是经常在并发时需要加读写锁。锁来锁去,不仅对性能有影响,写起来也感觉很烦。
标准库sync中有一个Map的数据结构,官方文档上是这么介绍的:

概括下就是说sync.Map,类似于map[interface{}]interface{},并且对于并发访问是安全的,不需要再额外加锁。大部分时候,用户应该使用map,sync.Map是专用的Map,对两种情形做了特殊的优化:

  1. key只写一次,读很多次
  2. 不同的线程访问的key互不相关

出于好奇,认真研读了下sync.Map的源码,其结构定义为

type Map struct {
    mu Mutex
    read atomic.Value
    dirty map[interface{}]*entry
    misses int
}

其中的关键就在与readdirty两个成员。通过源码,可以理解到sync.Map为什么是线程安全而且是专用的。

线程安全

  1. sync.Map中的value并不是直接以map[key] = value的形式存储的,而是map[key] = unsafe.Pointer,也就是存储了指向实际value的pointer。当删除某个key的值时,其实是把对应entry中的pointer置为nil(所有的赋值都是原子操作),自然不会造成map底层数据结构的组织性变动(一般是红黑树或者平衡树)。
  2. read是个readonly的结构,只能读取不能写。新增key时,都会写入到dirty这个map中(当然,mu这个锁会锁住),所以任何时候实际value不为空的元素,dirty只会比read
  3. readdirty在某些时候会相互转换:从read中读取key,miss超过一定次数时,read会直接将dirtymap中的值拿来赋给自身

专用
在读取read中的值时不需要加锁。如果value都放在read中(”每个key只写一次,而会读很多次"的情况就是这种情况),就不会有锁的开销。
如果不同线程访问的key不相交,那么跟上面说的一样,读取read中的值时不需要加锁;新增元素时,自带mu会生效;删除元素时,可能会mu加锁,或者对应的unsafe.pointer被置为空(不需加锁)

posted on 2020-03-14 23:59  ninja_ken  阅读(235)  评论(0编辑  收藏  举报