一直很好奇golan的map在运行过程中是以一个什么样的形式存在的,为什么并发不安全,抽空研究了下golang的源码,做个笔记。
map 的存在形式有两部分组成 hmap 和bucket 。
hmap的结构如下
type hmap struct {
count int // map的大小
flags unit8 //
B unit8 // 用来计算map的bucket大小 map的bucket大小(2^B)
noverflow unit16 // 溢出桶的大小 详细见 incrnoverflow
hash0 unit32 // hash 的种子
buckets unself.Pointer // 指向buckects数组的地址,可能为nil
oldbuckets unself.Pointer // 迁移用的(待补充)
nevacuate uintptr // 装载因子,用来判断扩容
extra * mapextra // hmap的额外信息,其中mapextra.nextOverflow指向预分配溢出桶的地址,当预分配用完之后变成nil
}
bucket的结构如下
type bmap struct {
tophash [buckectCnt]uint8 // key hash的高8位
}
注意: bucket 不仅仅放的是tophash 数组 紧接着是k1 k2 k3 … k8 v1 v2 v3 …v8 再接着是个指针(指针指向下一个溢出桶的地址) k1 k2 … v1 v2 为了避免内存的对齐, k v 的大小大于128字节 则存储的是指针
疑问 对于map[int]int 这类的map桶的大小是固定的 但是对于一个map[string]string 如何分配桶的大小
查询代码如下
hash := alg.hash(key, uintptr(h.hash0)) // key 的hash值
m := bucketMask(h.B) // hash 基数
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) // hash&m定位到第几个桶 根据桶大小算出对应桶的地址
if c := h.oldbuckets; c != nil { // 是否在搬迁 算出桶的地址(逻辑待确定)
if !h.sameSizeGrow() { // 使用旧桶的hash 基数
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) { // 判断当前桶是否已经搬迁
b = oldb
}
}
top := tophash(hash) // 高8位
for ; b != nil; b = b.overflow(t) { // 循环链表 直到溢出桶的地址为空 注意 overflow的定义 桶的大小-指针的大小
for i := uintptr(0); i < bucketCnt; i++ { // 遍历桶的tophash 跟 top 做对比 判断元素在桶的位置
if b.tophash[i] != top {
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) // 获取k的值 注意dateOffset = [8]unit8 = 64位 正好是tophash的大小
if t.indirectkey { // 如果k是个指针 则 寻找对应的值
k = *((*unsafe.Pointer)(k))
}
if alg.equal(key, k) {
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
if t.indirectvalue {
v = *((*unsafe.Pointer)(v))
}
return v
}
}
}
return unsafe.Pointer(&zeroVal[0])
现在可以画出map的内存布局图
初始化map
h.hash0 = fastrand() // 加入随机因子, 每个map 随机因子不同
// 选择合适的B
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 分配 内存空间 如果B 不为零
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b) // 计算出桶的数量
nbuckets := base
// 小于16个桶不分配溢出桶位置
if b >= 4 {
nbuckets += bucketShift(b - 4) // 计算出溢出桶的个数
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz { // 看看内存是否允许分配这么大的内存块
nbuckets = up / t.bucket.size
}
}
// 分配存 如果有回收的内存用回收的内存
if dirtyalloc == nil {
buckets = newarray(t.bucket, int(nbuckets))
} else {
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.kind&kindNoPointers == 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
// 如果有溢出桶 则nextOverflow指针指向溢出桶的位置
if base != nbuckets {
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) // nextOverflow指针指向溢出桶的位置
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) // 最后一个溢出桶改的位置
last.setoverflow(t, (*bmap)(buckets)) // 设置最后一个溢出桶的ptr 指向buckets的位置 形成一个环形 (在判断预分配的溢出桶是否已经使用完)
}
return buckets, nextOverflow
}
插入数据
// 判断是否正在写入
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 获取hash 值
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
// 设置正在写
h.flags |= hashWriting
again:
bucket := hash & bucketMask(h.B) // 找到第几个桶
if h.growing() { // 是否在扩容
growWork(t, h, bucket) // 搬迁当前桶 以及对应的溢出桶
}
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize))) // 获取当前桶的地址
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var val unsafe.Pointer
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == empty && inserti == nil { // 找到第一个空的位置记
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) // 非空 update
if t.indirectkey {
k = *((*unsafe.Pointer)(k))
}
if !alg.equal(key, k) { //判断key 值是否相等
continue
}
if t.needkeyupdate {
typedmemmove(t.key, k, key)
}
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 未找到key并且桶已满 判断是否需要扩容 然后然后在新的map 寻找插入位
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h) // 扩容
goto again // Growing the table invalidates everything, so try again
}
// 桶满 分配一个新的桶
if inserti == nil {
// all current buckets are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
val = add(insertk, bucketCnt*uintptr(t.keysize))
}
if t.indirectkey {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key) // 疑问 这个作用是什么
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectvalue {
val = *((*unsafe.Pointer)(val))
}
return val
返回 val 的地址,隐藏了最后一步写入动作(将值拷贝到指定内存区域)是通过底层汇编配合来完成的,在 runtime 中只完成了绝大部分的动作。
扩容
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) { // 判断是否需要调整大小
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets // 当前bucket 赋给old
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil) // 分配内存地址
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
if h.extra != nil && h.extra.overflow != nil {
if h.extra.oldoverflow != nil {
throw("oldoverflow is not nil")
}
h.extra.oldoverflow = h.extra.overflow
h.extra.overflow = nil
}
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}