golang sync.Map 原理
我们知道golang中的map是非gorountine安全的,所以在并发场景下使用map时需要加锁。但在高并发场景下,锁的争用会造成系统性能的下降。为了解决(或者说是缓解)这个问题,在golang 1.9引入了新的容器sync.map。sync.map中引入了锁,并使用了两个存储区域来对一些读场景进行加速,下面我们来分析一下他的源码。
sync.map在内部维护了两个数据结构:read和dirty,他们的底层都使用了map(builtin的那个hashmap)来实现的。关于这俩结构的用途,简单来说就是:
- read中的key是readOnly的(key的集合不会变,删除也只是打标记),value的操作全都可以原子完成,所以这个结构不用锁
- dirty是一个read的拷贝,用锁的操作在这做,如增加元素、删除元素等(dirty上删除是真删除)
好多分析博客里面直接说read是只读的我也是醉了……
但是里面还有些特例:
- 如果read中的key A先被删除(read中打expunged标记),然后再添加Key A,那么相当于“恢复”操作,不需要操作dirty
- 在某个没有任何dirty数据的时候,先删除read中的key A(read中打expunged标记),再添加key B,dirtyLocked()函数需要将read中所有“存在”的点拷贝到dirty,再在dirty中添加key B
从以上可以看出:
- 当dirty不存在的时候,read就是全部map的数据
- 当dirty存在的时候,dirty才是正确的map数据
看到这里可能觉得:为啥要把数据存两份?其实是为了dirty替换read而考虑的。
我们也可以把read看成是一个cache,当cache miss到一定数量的时候,dirty中的数据会被提升到read中去。但是决定哪些数据应该过去实在太费时了,倒不如时间换空间,read中的数据我在dirty中也存一份,提升的时候直接整个赋值就好了~
说了这么多,sync.map到底为啥这么搞呢?其实是基于一个假设:读远大于改。这种情况也符合大多数map的使用场景,大部分的读取可以通过读read来实现,而read是无锁的,没有争用,效率很高。但是在某些读写都很高的场景下这玩意是不适合的,从官方的benchmark也可以看出删除的性能不太好:毕竟新的dirty生成的时候需要遍历read,效率能好哪去……
其实做并发map最好的方法还是sharding,有个小哥曾写过一个PR,但是被婉拒了,github/concurrent-map。不知道出于什么考虑,golang团队并没有用这种简单有效的策略,不过在我们开发中是可以借鉴的。