上集回顾:map扩容
1.查找
在go map代码中,查找部分的逻辑应该是最简单的,唯一值得重视的一点就是,key所在的桶是新桶还是旧桶。按照2倍扩容的规则,查找旧桶则需要
1.1 校验和定位桶
查找部分同样需要做系列校验判断,需要注意一点是,在定位桶的位置时,需要旧桶有没有存在数据还没迁移,存在则是查找旧桶。
/*
:params t: 存储了key/value和桶大小等信息
:params h: map
:params key: key值
*/
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := abi.FuncPCABIInternal(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if asanenabled && h != nil {
asanread(key, t.key.size)
}
// 未初始化或者没有元素,直接返回
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
// map不允许并发读写,会触发该panic,这里是通过flags的标记位来判断是否正在进行写操作
if h.flags&hashWriting != 0 {
fatal("concurrent map read and map write")
}
// 确定桶位置
hash := t.hasher(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 检查检查旧桶有没有数据,有就要先处理旧桶,由于新桶总是旧桶的2倍,需要将对数-1
if c := h.oldbuckets; c != nil {
// 找到旧桶对应的索引地址
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
// tophash即首8位(高8位),用于快速比较。
top := tophash(hash)
...
2.查找key值
查找key值的逻辑相对简单,同样是遍历链桶和桶内数据,通过hash高8位和key值判断,来最终确认key的位置,返回对应的value值。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
bucketloop:
// 遍历链桶查找
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
// 遍历到最后一个元素,还是没找到退出循环
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// hash高8位相同,需要进一步判断key值是否一致
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if t.key.equal(key, k) {
// key值一致就返回对应value值
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
2.迭代
2.1 初始化迭代器
在初始化的时候,有一点是比较有趣的,每次的迭代的开始位置都是不一样的,从里面能找到答案,源码中是直接随机选择了开始迭代的hash数组的位置。
在思考作者为什么这样做的时候,我们回想一下在go map的设计中,除了人为控制迭代开始位置以外,还有哪些因素会影响到迭代的不确定性:
- 哈希种子,在初始化/空元素会对map的哈希种子进行重置(哈希种子会影响到对应的哈希值,即:桶位置)
- 扩容,在扩容期间,所有的元素虽然有规律进行迁移,但是元素位置已经是被打乱了。
在整个map的元素位置的不确定性中,如果只对map进行迭代(没有其他行为),会让用户误以为map内部的元素是有序的。所以作者加了最后一把锁,相同的map每次迭代开始的桶位置都是随机的。
主要控制随机的字段
- it.startBucket:这个是hash数组的偏移量,表示遍历从这个桶开始。
- it.offset:这个是桶内的偏移量,表示每个桶的遍历都从这个偏移量开始。
遍历过程:
- 从hash数组中第it.startBucket个桶开始,先遍历hash桶,然后是这个桶的溢出链。
- 之后hash数组偏移量+1,继续前一步动作。
- 遍历每一个桶,无论是hash桶还是溢出桶,都从it.offset偏移量开始。如果只是随机一个开始的桶,range结果还是有序的;但每个桶都加it.offset偏移,这个输出结果就有点扑朔迷离,大家可以亲手试下,对同一个map多次range)
- 当迭代器经过一轮循环回到it.startBucket的位置,结束遍历。
/*
:params t: 存储了key/value和桶大小等信息
:params h: map
:params it: 迭代器
*/
func mapiterinit(t *maptype, h *hmap, it *hiter) {
if raceenabled && h != nil {
callerpc := getcallerpc()
racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiterinit))
}
it.t = t
if h == nil || h.count == 0 {
return
}
if unsafe.Sizeof(hiter{})/goarch.PtrSize != 12 {
throw("hash_iter size incorrect") // see cmd/compile/internal/reflectdata/reflect.go
}
it.h = h
// 获取桶状态的快照
it.B = h.B
it.buckets = h.buckets
if t.bucket.ptrdata == 0 {
// 分配当前切片并记住指向当前切片和旧切片的指针。这将保持所有相关的溢出桶处于活跃状态。
// 即使在迭代时表增长和溢出桶添加到表中
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
// 随机定位:根据随机数,选择一个桶位置作为起始点进行遍历迭代
var r uintptr
if h.B > 31-bucketCntBits {
r = uintptr(fastrand64())
} else {
r = uintptr(fastrand())
}
// 这个是hash数组的偏移量,表示遍历从这个桶开始
it.startBucket = r & bucketMask(h.B)
// 这个是同桶内的偏移量,表示每个桶的遍历都是从这个偏移量开始
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// 迭代器状态
it.bucket = it.startBucket
//记住我们有一个迭代器。可以与另一个mapiteinit()同时运行。
if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
atomic.Or8(&h.flags, iterator|oldIterator)
}
mapiternext(it)
}
2.2 迭代hash数组的桶
2.2和2.3章节本来是合并的,但想着分开看的话,逻辑会显的更简单易懂。
这章主要针对准备下一个桶位置进行确认
- 遍历到最后,没有后续桶了,就结束迭代
- 存在,就需要检查是迭代新桶还是旧桶,并找到对应的桶位置
回顾一下几个重要的参数 iter结构体:
| 参数 | 含义 |
|---|---|
| it.startBucket | 开始的桶 |
| it.offset | 每个桶开始的偏移量 |
| it.bptr | 当前遍历的桶 |
| it.i | it.bptr已经遍历的键值对数量,i初始化为0,当i=8时表示这个桶遍历完了,将it.bptr移向下一个桶 |
| it.key | 每次迭代的key结果 |
| it.value | 每次迭代的value结果 |
| it.wrapped | 已经从hash数组的尾部到头部了,通俗点就是:hash数组我已经全部遍历过了 |
/*
:params it: 迭代器
*/
func mapiternext(it *hiter) {
h := it.h
// 校验
if raceenabled {
callerpc := getcallerpc()
racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiternext))
}
if h.flags&hashWriting != 0 {
fatal("concurrent map iteration and map write")
}
// 元素赋值
t := it.t
bucket := it.bucket
b := it.bptr
i := it.i
checkBucket := it.checkBucket
next:
if b == nil {
// 结束迭代
if bucket == it.startBucket && it.wrapped {
it.key = nil
it.elem = nil
return
}
// 下一个桶遍历前检查(主要是遍历新桶还是旧桶处理)
if h.growing() && it.B == h.B {
// 迭代器是在扩容中启动的,当扩容尚未完成。如果我们当前桶尚未填充(即旧存储桶尚未迁移)
// 那么我们需要遍历旧桶,只返回将要迁移到当前桶的桶
oldbucket := bucket & it.h.oldbucketmask()
b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 这里检查了该桶有没有已经迁移完毕,没就需要遍历该桶
if !evacuated(b) {
checkBucket = bucket
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
bucket++
// 已经从 hash 数组的结尾到开头了
if bucket == bucketShift(it.B) {
bucket = 0
it.wrapped = true
}
i = 0
}
...
}
2.3 迭代当前链桶
这一部分的代码之所以苦涩难懂,主要在扩容期间启动的迭代器,但迁移还没全部完成(需要做兼容)。 具体方案细节可以查看:迭代细节
带着疑问从代码中找答案
func mapiternext(it *hiter) {
...
// 桶内遍历
for ; i < bucketCnt; i++ {
// 确认随机开始位置(桶内需要随机开始位置)
offi := (i + it.offset) & (bucketCnt - 1)
// 嗯...因为随机开始位置的原因,isEmpty标志只能作为跳过处理,不能作为结束标志(旧桶同理)
if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
continue
}
// 计算key/value对应的值(因为实际上是一段连续空间,key/vlaue位置是逻辑位置,需要计算得到)
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
if checkBucket != noCheck && !h.sameSizeGrow() {
// 特殊情况:迭代器在扩容期间启动, 但扩容尚未完成
// 我们正在处理一个桶,它的旧桶还没有被迁移。或者至少,当我们启动桶的时候,它还没有被迁移
// 因此,我们迭代旧存储桶,跳过将会到另一个新存储桶的任何key(旧桶扩容为2倍)
if t.reflexivekey() || t.key.equal(k, k) {
// 如果旧存储桶中的item不是迭代中当前新存储桶的目标,请跳过它。
hash := t.hasher(k, uintptr(h.hash0))
if hash&bucketMask(it.B) != checkBucket {
continue
}
} else {
// 如果 k != k(NaNs),哈希不可重复。
// 我们需要一个可重复的随机选择,在迁移过程中向那个方向发送NaN。我们将使用 tophash的低位来决定
// 注意:这种情况就是为什么我们需要两个 evacuatedX和evacuatedY的evacuated tophash值,它们的低位不同。
if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
continue
}
}
}
// 这是可用的数据(还没被迁移),或者 key ==key,可以返回它
if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
!(t.reflexivekey() || t.key.equal(k, k)) {
it.key = k
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
it.elem = e
} else {
// 自迭代器启动以来,哈希表一直在扩容(迁移)。这个可用的数据对应的key现在已经发生了变化。
// 需要检查当前哈希表中的数据。
// 这段代码处理 key已被删除/更新或删除并重新插入的情况。
// 注意:我们需要重新获取key,因为它可能已经被更新为一个 equal(),但密钥不同。
rk, re := mapaccessK(t, h, k)
if rk == nil {
continue
}
it.key = rk
it.elem = re
}
it.bucket = bucket
if it.bptr != b { // 避免不必要的写障碍; see issue 14921
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
b = b.overflow(t)
i = 0
goto next
}