golang sync.Map 原理

24. 11 月 2017 golang 0

我们知道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团队并没有用这种简单有效的策略,不过在我们开发中是可以借鉴的。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.