本文不是完整的 sync.Map 介绍,适合有学习过一遍,但是还搞不太清楚的阅读。
你会不会经常不解
- dirty 和 read 里的数据是怎么保持一致的?
- 文档有提到,整体上是一个写操作一定会在读操作前同步完成的内存模型。但他是怎么实现这一点的?(和上面这个问题类似,即既然两个map不是同步的,为什么我们读操作永远能读到写好的结果,一致性方案是什么)
- 到底什么时候在read读写?什么时候在dirty读写?
以下列举个人总结的,按照合适的逻辑顺序 来描述 dirty 和 read 不同状态下的读写流程以及其中的转换关系。
初始状态
最平安的正常流程:read=nil
- 正常写:覆写和新写直接加锁附加到dirty里边。
- 正常读:都直接加锁读 dirty,read里边读不到。miss++;如果 dirty 也读不到,那么就是返回nil。
- miss == len(dirty),触发 「promote」流程:dirty底层的map让read去引用,dirty = nil。
read已建立
read != nil, dirty = nil, amend = false 状态下的读写:
-
正常读:直接走 read,这是最爽的时候,原子操作也能确保你被读的这个entry暂时不会被写给触发扩容;没读到,小要点是这个时候 amend = false,所以也不会过dirty直接返回nil。
-
正常覆写:read 有,直接交换,不经过 dirty
-
首次新写:触发「rebuild」重建dirty流程,read有的东西我dirty一定不能没有,大概是整个read给复制到dirty,但是排除已经删除的。同时赋值
amend = true重建过程的细节是:expunged 标记在这个时候创建。因为整体删key是软删除机制,不会直接底层删除(摊销防止并发问题),是最后到 dirty再promote的时候才会集中清理,或者这个key重新被写的时候被改变状态。在这种设计下,expunged被用来区分nil(syncmap是类型不安全的,kv均为any,这里的话即nil是value的一个合理的实际的值,绝对不等于“被删除”)。
这个状态我们可以观察到,不一致的地方在于 read 他是优先的,所以操作可能是 dirty 没有的。不仅仅这个状态,下一个状态也可能有。
整体是怎么避免这个问题的呢?
- 首先我们 read 是优先读的
- 其次 dirty 一定会在最终复制全量的 read 数据,而dirty在后面会说,他又会代替原来的map。 基于这两个点,我们不必担心read状态会不一致,反正最终会是一致的。
dirty已重建
read != nil, dirty != nil
- 读:其他都同上。唯一不同是,如果没读到,会加锁进dirty。(因为可以留意下,下面没rebuild之前,是整个流程中唯一
amend=false的状态,所以成不用检查dirty直接就可以判断不存在了) - 新写:read肯定没有,加锁进 dirty,dirty直接写即可。
- 覆写:分read有无两种情况。read部分同上;而这个时机下有可能需要加锁只更新dirty,因为上面的新写导致dirty会更新,所以有了第二种情况。
以上流程当且仅当 miss 次数还没触发 promote 的时候才会正常运作。
这里可以观察到,read和dirty一开始是完全一致的,但是新写只会在dirty加上,然后后续覆写也有部分只更新ditry,整体流程符合 amend = true。
然后正是这样,所以才会导致,读的时候加锁进dirty才能读到,此时 miss++ ,等待下一次promote的触发,重回状态2。这也是 sync.Map 的最核心的一个流程。
同时,sync.Map使用的核心,就是不适合大量新写。原因就在这里,新写可能导致大量触发miss和重建。
可能的并发流程
或者是回答问题:为什么 sync.Map 是并发安全的?
- 在「状态一」下:全都是对 dirty 的操作,而这个操作是加锁的。完全安全
- 状态二:读和覆写?原子操作+本质上是换元素而没有新增,不会触发扩容。新写的话,因为是dirty所以没问题。
- 状态三:可以发现基本也是同上。在read不会有新写,dirty下的操作是安全的。
所以整体的结论,简单版就是read没有新写,dirty下做增长set,这边是安全的。然后通过迁移操作确保
上面的结论其实是存在一部分简化的,我们没有考虑到迁移过程造成的影响。但是迁移过程其实也不用担心,他们都是加锁才能进的流程。 missLocked 和 dirtyLocked
总结
整体上重点
- 一个是整体状态流转循环。状态一开始,然后不断2-3状态之间循环的过程,循环的过程是能保持最终一致的。
- 另一个是状态3结束后的同步,也就是接上循环的过程。
- 基于以上框架再去处理并发问题
我想,设计之初大概也是这么考虑的
看了挺久了,写得好像有点自以为是了。有什么问题还是希望能指正。