这篇文章,主要查看goland中map的put函数的大致源码及流程,废话不多说,直接上代码
//这个就是put过程中最重要的函数
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//一些安全验证,注释略过,不看
/**
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := getcallerpc()
pc := abi.FuncPCABIInternal(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.key.size)
}
if asanenabled {
asanread(key, t.key.size)
}*/
//判断是否并发写
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
//调用hash函数获取key对应的hash值
hash := t.hasher(key, uintptr(h.hash0))
//修改flags状态,标志为正在写,避免后续线程并发写,所以map只是单线程使用的,多线程环境用syn.map
h.flags ^= hashWriting
//如果还没创建桶数组,就创建
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
//算出应该存到那个数组,h.B是跟数组数量相关的数(先了解hmap结构)
bucket := hash & bucketMask(h.B)
//判断map是否处于扩容状态--(go的map使用的扩容机制的rehash而不是一次性hash)
if h.growing() {
growWork(t, h, bucket)
}
//确定key要存放到那一个bmap
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
//hash值的高八位
top := tophash(hash)
var inserti *uint8 //key在bmap里的位置
var insertk unsafe.Pointer //key的地址
var elem unsafe.Pointer //value的地址
bucketloop:
for {
//遍历选中的bmap
for i := uintptr(0); i < bucketCnt; i++ {
//对比key值的高8位,如果不相等,进入if
if b.tophash[i] != top {
//如果该位置未被使用且key还未找到位置,使用该位置
if isEmpty(b.tophash[i]) && inserti == nil {
//设置key在数组里的位置,通过位移找到对应的key和value的地址
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
/**
这里比较有意思,在前面的isEmpty判断中,是判断是否<=1
而这里的emptyRest的值其实是0
0和1其实都表示了当前位置是空的,当是0还表示了,这个位置之后没有空位置了(包括后续的溢出桶)
所以在跳出循环时,标记的最后的一个位置
那为什么要找最后一个位置而不是找到一个能用的位置就退出呢?当然是为了确认key是否已经存在
*/
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
//高8位比较相等的情况
//找到对应key的位置
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
//比较该位置上的key和传入的key是否相同
if !t.key.equal(key, k) {
continue
}
//比较相同,也就是key已经存在于map,进行赋值操作
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done //跳转到结束位置
}
//执行到这里,说明在当前的bmap中,没有找到key,当不确定key是否在溢出桶中
//获取溢出桶的位置,如果没有溢出桶,跳出循环,否则,遍历溢出桶,再次执行遍历流程
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
/**
到了这里,有两种状态:一是找到了一个空位置,并记录了地址,二是bmap全是满的,变量现在还是空的
注意的是,如果key是已存在的,已经goto done了,不会来到这里
*/
//判断map的负载因子是否达到6.5,溢出桶是否太多了。如果是,进行扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // 回到起点,重新走一次流程
}
//没有找到可能使用的桶的情况,重新创建一个桶使用
//其实就是多创建了一个bmap做溢出桶
if inserti == nil {
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
//一些列的赋值操作,没什么好说的
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = 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.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
流程大致总结:
1、 安全性检查、并发写检查
2、标记正在写,计算hash值,找到对应的bmap
3、遍历对应的bmap及其溢出桶,寻找合适的位置
4、如果key已经存在,更新对应的value
5、如果key不存在,将最后一个位置分配给key,若已无位置,创建一个bmap做溢出桶
接下来再来看看扩容的一个流程:
当负载因子到了6.5或者溢出桶太多时,会进入hashGrow函数:
func hashGrow(t *maptype, h *hmap) {
bigger := uint8(1)
//这里判断这次扩容是因为负载因子到了6.5还是因为溢出桶太多
if !overLoadFactor(h.count+1, h.B) {
//溢出桶太多了,负载因子还没到6.5
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets
//bigger是1或者0,也就是说,扩容到2倍或者不变,对应负载因子和溢出桶两种情况
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
//判断当前是不是迭代的状态,功能略过
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// 对hmap进行属性值的更新
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
//对extra进行nil判断,进行移值
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
}
}
可以看出,hashGrow并没有干多少事,我们再来看看put好delete过程中都出现的growWork函数:
func growWork(t *maptype, h *hmap, bucket uintptr) {
//将bmap搬迁到新的bucket数组
evacuate(t, h, bucket&h.oldbucketmask())
// 搬迁未完成,再搬迁一个
if h.growing() {
evacuate(t, h, h.nevacuate)
}
//可以看到,每次growWork其实最多可以搬迁两个bmap到新数组
}
接下来详细看看evacuate函数:
// 先了解一个结构体,官方注释称它是一个撤离地点
type evacDst struct {
b *bmap // 当前目标桶
i int // key-value在b中的索引位置
k unsafe.Pointer // key的地址
e unsafe.Pointer // value的地址
}
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
//获取要搬迁的bmap
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
//算一下之前有多少个桶
newbit := h.noldbuckets()
//判断bmap里面有没有东西,也就是要不要搬迁
if !evacuated(b) {
var xy [2]evacDst
x := &xy[0]
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
x.k = add(unsafe.Pointer(x.b), dataOffset)
x.e = add(x.k, bucketCnt*uintptr(t.keysize))
//如果不是等量扩容,多创建一个
if !h.sameSizeGrow() {
y := &xy[1]
y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
y.k = add(unsafe.Pointer(y.b), dataOffset)
y.e = add(y.k, bucketCnt*uintptr(t.keysize))
}
//遍历bmap及其溢出桶
for ; b != nil; b = b.overflow(t) {
//bmap中第一个key和value的地址
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
//遍历当前的bmap
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
top := b.tophash[i]
//如果该位置未空,做一下标记,开始遍历下一个
if isEmpty(top) {
b.tophash[i] = evacuatedEmpty
continue
}
//错误问题,不深究
if top < minTopHash {
throw("bad map state")
}
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
//如果是翻倍扩容,需要计算key-value应该存那个bmap
/**
这里简单的说一下通过hash寻找下标的算法,其实跟java一样,知道的直接跳过
1、通过hash函数获取到key对应的hash值
2、要注意一点是,hmap里的bmap数组长度是2的幂次方,二进制表示计算00...0010..00
3、数组长度n是一个只有一位是1的数字,而我们知道实际范围是0-(n-1)
4、再了解&(位与)操作,n&m<=Math.min(n,m)
5、所以如果计算hash&(n-1)就可以获得一个在0-(n-1)范围内的值
6、扩容是扩到原来的两倍,也就是n左移一位,那(n-1)其实就是多了一个1
7、那只要判断hash值对应位是1还是0就可以知道key在新数组是在原位置或是加了n/2
*/
if !h.sameSizeGrow() {
hash := t.hasher(k2, uintptr(h.hash0))
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
useY = top & 1
top = tophash(hash)
} else {
if hash&newbit != 0 {
useY = 1
}
}
}
//错误情况,不深究
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}
//开始搬迁,主要这里是双重循环内部
//第一层循环是遍历bmap及其溢出桶
//第二层循环是遍历一个bmap里的数组
//下面就是就原bmap数组里的一个key-value转移新的bmap里
b.tophash[i] = evacuatedX + useY
dst := &xy[useY]
if dst.i == bucketCnt {
dst.b = h.newoverflow(t, dst.b)
dst.i = 0
dst.k = add(unsafe.Pointer(dst.b), dataOffset)
dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
}
dst.b.tophash[dst.i&(bucketCnt-1)] = top
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2
} else {
typedmemmove(t.key, dst.k, k)
}
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
dst.i++
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// 对应迁移完的bmap,进行标记,便于GC的清理
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
//如果这次搬迁是按照h.nevacuate做的,需要标记更新nevacuate
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
扩容搬迁小总结:
1、 在进行put和delete时,都会进行gorwWork函数进行搬迁
2、gorwWork会搬迁两次,一次是搬迁当前使用到的bmap,一次是按照进度搬迁(可能已经被搬迁了或为空)
3、evacuate函数是真正的搬迁函数,它每次都会搬运一个bmap及其溢出桶到新的数组
最后再讲一下golang的渐进式rehash:
1、 说到渐进式rehash,redis的hashmap跑不了,为了避免一次性hash的停顿,redis使用了rehash将搬迁分摊给每一次操作
2、与只对比的就是JAVA的hashmap了,java的hashmap使用一次性的hash
3、所以就有这样一道面试题:为什么java不使用渐进式的rehash呢?
先了解一下java的hashmap底层,它用的是一个数组,每个数组元素是一个链表或红黑树
如果使用渐进式的rehash,首先的问题是,原数组无法立即得到释放,多占了一部分空间,而且可能会滞后很久
其次就是rehash会带来比较高的复杂性,总体下来rehash并不完全优于一次性hash
那问题来了,为什么golang的map可以rehash?
这其实是设计上的问题,goland使用hmap和bmap两种结构,让map天然就是分开存储的,转移完一个bmap,就可以标记让GC清理了,这样就不存在长时间占用要释放的空间的问题了。而且对应evacuate函数来说,每次搬运的都是一个整体(bmap),也不存在高复杂度。