这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战
基本罗列一下 go 原生提供最基本的数据机构。面试向供。建议有看过源码的看此篇文章,达到快速记忆,没看过的建议去看源码。
map
- 查找过程?
- delete map key 会引发什么性能问题?
- sync.Map 是怎么保证并发安全的?
查找过程
-
在初始化时,会选定一个 hasher 函数。
t.hasher(key, uintptr(h.hash0))获取到 hash 值 -
hash 按位与 → 拿到低位的 B 位数 (和容量有关) 的值 ⇒ bucket,所在的桶
-
如果 oldbuckets 不为空,先在 old 里面查找:
- 先会确定 key 在 oldbuckets 中的位置
- 如果 oldbucket 还没有搬迁到新的 bucket,那就在 oldbuckets 中查找
- 否则就在 bucket 中查找
-
bucket 中的查询 (不管是 oldbuckets 还是 bucket,均是如此):
- 按位与取出 高8位 的 tophash 值 → 可以快速查找 bucket 中有没有 key 的存在
- 如果存在,就去找到 tophash 的 index 位置 (向后偏移 i 个 keysize) → key 的位置
- 判断 key 是否相等 (hash 冲突校验) → 相等就按照同样的偏移找到 value 的位置
- 找到就返回
- 如果在当前的 bucket 中没有找到,则去
overflow(t)中寻找 (溢出桶中寻找)
delete key
- 删除依然还是先要找到这个 key 的位置 → 查找过程
- 找到位置 → 对 key/value 清零
- count - 1,总数 - 1 这个很正常
- 对应的 tophash → Empty
sync.Map
map 是没有保证并发读写的,会直接 panic。这个在源码级别已经控制住了,如果发现正在被读,你进行写操作,直接 panic。
先说说 sync.Map 使用场景:
- 只会增长的缓存系统中,一个 key 只写入一次而被读很多次
- 多个 goroutine 为不相交的键集读、写和重写键值对
说白了就是尽量不要并发对一个 key 进行频繁的读写。
然后看看实现:
- 只读的 read 字段,可写的 dirty → 对 read 是不需要加锁的
- 优先从 read 字段读取、更新、删除
- miss 次数多了之后,将 dirty 数据提升为 read,避免总是从 dirty 中加锁读取
- double-checking。加锁之后先还要再检查 read 字段,确定真的不存在才操作 dirty 字段
- 删除一个键值只是打标记,只有在提升 dirty 字段为 read 字段的时候才清理删除的数据