map是golang的内置数据类型之一,日常工作中用起来真的是非常方便。可它也有个明显的不足之处,就是经常在并发时需要加读写锁。锁来锁去,不仅对性能有影响,写起来也感觉很烦。
标准库sync中有一个Map的数据结构,官方文档上是这么介绍的:
概括下就是说sync.Map,类似于map[interface{}]interface{}
,并且对于并发访问是安全的,不需要再额外加锁。大部分时候,用户应该使用map,sync.Map是专用的Map,对两种情形做了特殊的优化:
- key只写一次,读很多次
- 不同的线程访问的key互不相关
出于好奇,认真研读了下sync.Map的源码,其结构定义为
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface{}]*entry
misses int
}
其中的关键就在与read
和dirty
两个成员。通过源码,可以理解到sync.Map为什么是线程安全而且是专用的。
线程安全
- sync.Map中的value并不是直接以map[key] = value的形式存储的,而是map[key] = unsafe.Pointer,也就是存储了指向实际value的pointer。当删除某个key的值时,其实是把对应entry中的pointer置为nil(所有的赋值都是原子操作),自然不会造成map底层数据结构的组织性变动(一般是红黑树或者平衡树)。
read
是个readonly的结构,只能读取不能写。新增key时,都会写入到dirty
这个map中(当然,mu这个锁会锁住),所以任何时候实际value不为空的元素,dirty
只会比read
多read
和dirty
在某些时候会相互转换:从read
中读取key,miss超过一定次数时,read
会直接将dirty
map中的值拿来赋给自身
专用
在读取read
中的值时不需要加锁。如果value都放在read
中(”每个key只写一次,而会读很多次"的情况就是这种情况),就不会有锁的开销。
如果不同线程访问的key不相交,那么跟上面说的一样,读取read
中的值时不需要加锁;新增元素时,自带mu会生效;删除元素时,可能会mu加锁,或者对应的unsafe.pointer被置为空(不需加锁)