Sync.map
https://zhuanlan.zhihu.com/p/599178236
- 基于读写分离机制,读操作不加锁,写操作加锁。
- 数据结构中维护2个map,read和dirty,read用于读,作为dirty的快照,dirty用于写数据,通过互斥锁mu实现写数据的原子操作。
- 查询:
先从read里查询,read里没有,从dirty里查询,dirty里没有,返回nil,操作dirty需要加锁。查询操作涉及到与 dirty map 的交互,misses 加一,执行missLocked流程 如果倘若 miss 次数大于等于 dirty map 中存在的 key-entry 对数量,则使用 dirty map 覆盖 read map,并将 read map 的 amended flag 置为 false - 写操作:
先查询read中存在且不为expunged状态,直接用cas更新value,若read和dirty 都不存在,则在 dirty map 中插入新 key-entry 对,并且保证 read map 的 amended flag 为 true. - 删除: read存在,则直接cas删除,read不存在,从dirty中取出元素,并删除对应的key-value,删操作需要和 dirty map 交互,需要走进 3.3 小节介绍的 missLocked 流程;
- 遍历: 在遍历过程中,倘若发现 read map 数据不全(amended flag 为 true),会额外加一次锁,并使用 dirty map 覆盖 read map 遍历 read map(通过步骤(1)保证 read map 有全量数据),执行用户传入的回调函数,倘若某次回调时返回值为 false,则会终止全流程.
- 元素状态分为,正常,软删除,硬删除。
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
写入流程
整体流程如下:
- 1)如果在 read 里能够找到待存储的 key,并且对应的 entry 的 p 值不为 expunged,也就是 没被删除时,直接更新对应的 entry 即可。
- 2)若第一步没有成功:要么 read 中没有这个 key,要么 key 被标记为删除。则先加锁,再 进行后续的操作。
- 3)再次在 read 中查找是否存在这个 key,也就是 double check 一下,这也是 lock-free 编程 里的常见套路。
- 4)如果 read 中存在该 key,但 p == expunged,说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值。此时:a. 将 p 的状态由 expunged 更改为 nil;b. dirty map 插入 key,然后直接更新对应 的 value。
- 5)如果 read 中没有此 key,那就查看 dirty 中是否有此 key。如果有,则直接更新对应的 value,这时 read 中还是没有此 key。
- 6)最后一步,如果 read 和 dirty 中都不存在该 key,则:a. 如果 dirty 为空,则需要创建 dirty,并从 read 中复制未被删除的元素;b. 更新 amended 字段,标识 dirty map 中存在 read map 中 没有的 key;c. 将 k-v 写入 dirty map 中,read.m 不变。最后,更新此 key 对应的 value。
查询流程
- 1)首先是 fast path,直接在 read 中找,如果找到了直接调用 entry 的 load 方法,取出其中 的值。
- 2)如果 read 中没有这个 key,且 amended 为 false,说明 dirty 为空,那直接返回空和 false。
- 3)如果 read 中没有这个 key,且 amended 为 true,说明 dirty 中可能存在要找的 key。当 然要先上锁,再尝试去 dirty 中查找。在这之前,仍然有一个 double check 的操作。若还是没有在 read 中找到,那么就从 dirty 中找。不管 dirty 中有没有找到,都要“记一笔”,因为在 dirty 被提 升为 read 之前,都会进入这条路径。