golang map部分原理源码个人走读-附个人理解过程图解

近期再写map的demo时出现了下面一段报错,于是带着疑惑去看了一下源码

目的:主要想知道为啥map不让并发读写

fatal error: concurrent map read and map write

一.map的数据结构

先有个印象,后续会详细介绍

// A header for a Go map.
type hmap struct {
	
	count     int // 键值对数量
	flags     uint8
	B         uint8  // 
	noverflow uint16 // 
	hash0     uint32 // 

	buckets    unsafe.Pointer 
	oldbuckets unsafe.Pointer 
	nevacuate  uintptr      

	extra *mapextra // optional fields
}

type mapextra struct {

	overflow    *[]*bmap // 溢出桶
	oldoverflow *[]*bmap // 扩容时老的溢出桶

	nextOverflow *bmap // 下一个可用的溢出桶
}

bucket结构如下

// A bucket for a Go map.
type bmap struct {
	tophash [bucketCnt]uint8 // bucketCnt=8
}

实际上编译的时候会加点料,就是上图画的那个样子

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

二.创建map

func main() {
	mp := make(map[int]int, 32)
	mp[1] = 1
}

直接使用go tool compile -S main.go查看

1.makemap() 主要是初始化map
func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()

	// 找到一个B,使得map的装载因子(后续介绍)再正常范围内
    // 也就是 一个bucket能装8个key-val,根据你设定的key-val数量确定bucket数量
    // 又因为2^B=buckets 以此确定B的大小,有兴趣自己看源码
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	if h.B != 0 {
		var nextOverflow *bmap
        // 创建bucket数组
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
            // 当B>=4时创建溢出桶overflow
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

三.前置知识(纯个人理解,我关注的重点不在这)

1.哈希函数

(纯个人实力有限,粗略理解)查找key过程使用哈希函数,我没过多了解,就是大概理解成数学中的函数y=f(x)(个人理解),就像下面的第一张函数图一样,x可以随意取值,经过f(x)计算后得到的值范围没有x的取值范围大,也就是说把x理解成输入源,y理解成输出源,这样容易出现可能不同的x值,算出来的y值一样,例如y=x^2,x取+-1,结果都是1,简单理解成哈希碰撞。

2.解决哈希碰撞办法
2.1 开放地址法

核心思想主要是依次探测和比较数组中的元素以判断目标键值对是否存在哈希表中

个人理解:秉持着跟谁都不争的态度,有人住了就往下接着找个安静的地方呆着(按照索引大小向下)

2.2 拉链法

核心思想主要是发生冲突了就用链表连起来。

golang的map实现中就结合了这两种方法

四.查找key的过程,我剔除了无关的过程

再回顾一下bmap(bucket)的结构,只是一个tophash [bucketCnt]uint8,这个数组只是存储了key的对应哈希值的前8位,用于加速查找key和表示key处于某个状态

首先,确定key在哪个桶,也就是用key计算出的hash值 % buckets数组的长度的结果作为桶的索引

其次,index = x % y = x & (y -1) = hash & (2^B -1),假设B=2,那么index = hash & 0000 0011,也就是说index的值只与hash的后2位有关。

1.mapaccess1()
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if h == nil || h.count == 0 {
		if t.HashMightPanic() {
			t.Hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
    // 看到一开始想知道为啥的报错信息了 不能并发读写map
	if h.flags&hashWriting != 0 {
		fatal("concurrent map read and map write")
	}
    // 计算key对应的hash值
	hash := t.Hasher(key, uintptr(h.hash0))
    // 2^B - 1 后面做与运算
	m := bucketMask(h.B)
    // 用hash值与m做与运算求key所在的bucket  简单点理解就是做%模运算
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.BucketSize)))
    // 计算hash值前8位
	top := tophash(hash)
bucketloop:
	for ; b != nil; b = b.overflow(t) { // 外层遍历上面找到的bucket以及bucket内的overflow(溢出桶)
		for i := uintptr(0); i < bucketCnt; i++ { // 内层循环遍历bucket内的8个key-val
			if b.tophash[i] != top {// 如果hash前8位不相等
				if b.tophash[i] == emptyRest { // 这个emptyRest表示第i个后面的key-val都是空,不用找了,省点力气
					break bucketloop
				}
                // 接着比较下一个
				continue
			}
            // 找到了hash前8位相等的对应的key
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			if t.IndirectKey() {
				k = *((*unsafe.Pointer)(k))
			}
            // 接着比较bmap存储的key是否是自己想要找的
			if t.Key.Equal(key, k) { // 相等返回val
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
				if t.IndirectElem() {
					e = *((*unsafe.Pointer)(e))
				}
				return e
			}
		}
	}
    //没有找到就返回该map存储类型对应的零值
	return unsafe.Pointer(&zeroVal[0])
}
2.tophash() 计算hash值的前8位,并且给其加上一个minTopHash,主要是为了区分正常key的状态和一些key的特殊状态
// 空的 cell,也是初始时 bucket 的状态
empty          = 0
// 空的 cell,表示 cell 已经被迁移到新的 bucket
evacuatedEmpty = 1
// key,value 已经搬迁完毕,但是 key 都在新 bucket 前半部分,
// 后面扩容部分会再讲到。
evacuatedX     = 2
// 同上,key 在后半部分
evacuatedY     = 3
// tophash 的最小正常值
minTopHash     = 4
func tophash(hash uintptr) uint8 {
	top := uint8(hash >> (goarch.PtrSize*8 - 8))
	if top < minTopHash {
		top += minTopHash
	}
	return top
}

花了个个人理解的过程图,如下:

五.获取map值的两种方式

1.mapacees2()

第一种获取值的方式和上面四描述的一样,第二种也就是比第一种多了一个bool的返回值,表示key有没有命中。(先忽略map处于扩容的时候获取值,后续在展开)

func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
	if h == nil || h.count == 0 {
		if t.HashMightPanic() {
			t.Hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0]), false
	}
	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)))

	top := tophash(hash)
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
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			if t.IndirectKey() {
				k = *((*unsafe.Pointer)(k))
			}
			if t.Key.Equal(key, k) {
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
				if t.IndirectElem() {
					e = *((*unsafe.Pointer)(e))
				}
				return e, true
			}
		}
	}
	return unsafe.Pointer(&zeroVal[0]), false
}

六.扩容和遍历过程

1.mapiterinit()

这个函数主要是为了初始化hint这个结构体

type hiter struct {
	key         unsafe.Pointer //key指针 
	elem        unsafe.Pointer // value指针
	t           *maptype // map类型
	h           *hmap // map header
	buckets     unsafe.Pointer // 初始化时指向的bucket
	bptr        *bmap          // 当前遍历到的 bucket
	overflow    *[]*bmap       // 当前遍历到的 bucket的overflow(溢出桶)
	oldoverflow *[]*bmap       // 老的bucket的overflow(溢出桶)
	startBucket uintptr        // 起始遍历的bucket索引
	offset      uint8          // 遍历开始的cell编号 每个bucket有8个cell(key-val键值对)
	wrapped     bool           // 是否从头遍历了
	B           uint8             // B的大小
	i           uint8             // 当前遍历的cell编号
	bucket      uintptr          // 当前指向的bucket
	checkBucket uintptr           // 因为扩容,需要检查的bucket
}
func mapiterinit(t *maptype, h *hmap, it *hiter) {
    // r 为随机数
	var r uintptr
	if h.B > 31-bucketCntBits {
		r = uintptr(fastrand64())
	} else {
		r = uintptr(fastrand())
	}
    // r & (2^B - 1)  这个也就是r%bucket数组的长度 结果就是0-2^B-1
    // 决定从哪个bucket遍历
	it.startBucket = r & bucketMask(h.B)
    // 这个(bucket-1)=0000 0111 r右移B位之后&(bucket-1) 的结果就是0-7
	it.offset = uint8(r >> h.B & (bucketCnt - 1))

	// iterator state
	it.bucket = it.startBucket


	mapiternext(it)
}
1.扩容条件和种类

条件1:装载因子loadFactor = 总key-val数量 / buckets数组的长度(2^B)。当loadFactor>6.5时会触发扩容。至于为啥是6.5,注解里面写的很清楚

//  装载因子       溢出桶占比   键值对占比    key的命中占比  key没有命中占比
//  loadFactor    %overflow  bytes/entry     hitprobe    missprobe
//        4.00         2.13        20.77         3.00         4.00
//        4.50         4.05        17.30         3.25         4.50
//        5.00         6.85        14.77         3.50         5.00
//        5.50        10.55        12.94         3.75         5.50
//        6.00        15.27        11.67         4.00         6.00
//        6.50        20.90        10.79         4.25         6.50
//        7.00        27.14        10.15         4.50         7.00
//        7.50        34.03         9.73         4.75         7.50
//        8.00        41.10         9.40         5.00         8.00

 条件2:overflow溢出桶太多了:当overflow的数量大于等于2^B(也就是buckets数组的长度)

这个条件1主要是为了防止buckets的每一个bucket都装满了key-val,也就是key-val太多了,buckets数组装的太满。

这个条件2主要是为了弥补条件1的不足,就是会有这样的情况,当bucket的key-val很多的时候,但是loadFactor还没超过6.5也就触发不了条件1扩容2,然后不停的插入和删掉溢出桶的key-val,导致每个溢出桶里面的key-val都分布的不满不均匀(也就是房子很多,住的人很少),这样的查找效率就会很低

扩容种类1:增量扩容,也就是B+1,就是创建一个新的长度为原来buckets数组长度的两倍

对应条件1是因为key-val太多,bucket太少,所以就创建多点bucket

扩容种类2:等量扩容,也就是创建一个新的长度等于原来buckets数组

条件2是因为overflow溢出桶太多,那就重新创建bucket,然后整理overflow的key-val就行

图解:

需要注意的是增量扩容,扩容前的bucket里面的key-val会被打散分散到两个bucket。比如扩容前bucket0里面有两个key-val分别是key-val1和key-val2,那么扩容后会被分散到bucket0和bucket(2^B).下面解释为啥会被分散到这两个桶:

首先,确定key在哪个桶,也就是用key计算出的hash值 % buckets数组的长度的结果作为桶的索引

其次,index = x % y = x & (y -1) = hash & (2^B -1),假设B=2,那么index = hash & 0000 0011,也就是说index的值只与hash的后2位有关。

最后,增量扩容之后key的存放位置其实就是直接与hash的B+1位有直接关系。

例子:假设原本B=2,key1的hash值后三位101,key2的hash值后三位是001,因为B=2,所以就取后两位作为key的存放桶索引,因为两个key的后两位都是01也就是放到索引为1的bucket,扩容之后B=3,然后key1就放到了101也就是索引为5的bucket,key2也就放到了001也就是索引为1的bucket。

具体的扩容过程后续写、插入、修改key时展开,接下来接着看遍历过程。

2.遍历过程
mapiternext()
func mapiternext(it *hiter) {
	h := it.h

	if h.flags&hashWriting != 0 {
		fatal("concurrent map iteration and map write")
	}
	t := it.t
	bucket := it.bucket // 如果处于扩容过程此bucket是扩容后的bucket
	b := it.bptr
	i := it.i
	checkBucket := it.checkBucket

next:
    // 一开始b默认就是nil
	if b == nil {
		if bucket == it.startBucket && it.wrapped { // 表示当前的遍历的bucket就是一开始遍历的bucket并且已经从头遍历过一轮buckets 例如 startBucket = 3 遍历顺序从 3 4 0 1 2 3重新回到原始的桶位置 此时还没找到就返回
			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++
		if bucket == bucketShift(it.B) { // 检查bucket++后是否到了最后一个bucket
			bucket = 0 
			it.wrapped = true // 标记从头遍历
		}
		i = 0
	}
	for ; i < bucketCnt; i++ { // 遍历选中的bucket的8个cell
		offi := (i + it.offset) & (bucketCnt - 1) // 随机决定从哪个cell往下从头遍历
		if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty { // 如果当前的cell是空 跳过
			continue
		}
		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.ValueSize))
		if checkBucket != noCheck && !h.sameSizeGrow() { // 如果正在扩容并且不是等量扩容
			if t.ReflexiveKey() || t.Key.Equal(k, k) {
				hash := t.Hasher(k, uintptr(h.hash0))
				if hash&bucketMask(it.B) != checkBucket { // 过滤出老桶中扩容后属于新桶的key-val
					continue
				}
			} else {

				if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
					continue
				}
			}
		}
		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 { // 老桶迁移完就用新通数据
			rk, re := mapaccessK(t, h, k)// 检查一下数据在新桶是否存在
			if rk == nil {
				continue // key has been deleted
			}
			it.key = rk
			it.elem = re
		}
		it.bucket = bucket
		if it.bptr != b { // avoid unnecessary write barrier; see issue 14921
			it.bptr = b
		}
		it.i = i + 1
		it.checkBucket = checkBucket
		return
	}
	b = b.overflow(t) // 遍历下一个overflow
	i = 0 // cell索引置零
	goto next
}

个人理解的动画图:

假设当前map正在扩容,B此时等于2,buckets数组长度是4,oldmap的部分bucket数据为迁移如下图所示

假设这个时候开始遍历,情形如下图所示:

1.假设startbucket=1,startIndex=2,因为map正在扩容,这个时候遍历bucket1时会先去检查bucket1所属的老桶也就是左边的old bucket1,看看old bucket1数据有没有迁移完,没有迁移完就使用old bucket1数据右边 new bucket1的数据(也就是扩容之后数据还是存放在桶1的数据,主要看低B位数据,原因在上面扩容说过了),因为当前左边的old bucket1的数据已经迁移完成,所以就正常使用右边new bucket1的数据就行。遍历bucket内的数据按照startindex开始的顺序往下,到结尾就回到开头一直到再次回到startindex的位置。所以此时遍历得到的值如下:

2.此时完成循环遍历到右边的new bucket2,按照1所述的规则,检查到new bucket2的数据原本属于左边的old bucket0,又因为左边的old bucket0的数据还未迁移,所以此时会去old bucket0中找到扩容后数据右边new bucket2的数据(也就是lowbit=10)。此时遍历得到的值如下:

3.接着外层循环遍历到右边的new bucket3,按照规则, 此时遍历得到的值如下:

4.接着外层循环遍历到右边的new bucket0,此时遍历得到的值如下:

七.写和更新过程

1.mapassign()

注意:1.map的扩容不是一次性的,而是“渐进性”,也就是一点点的把数据搬到新的bucket。随着key的插入、更新、删除都会触发数据搬运,这三种操作一次最少搬运一个bucket数据,最多两个。2.value的插入和更新使用汇编完成。

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if h == nil {
		panic(plainError("assignment to entry in nil map"))
	}

    // 如果当前有goroutine正在写map 直接fatal
	if h.flags&hashWriting != 0 {
		fatal("concurrent map writes")
	}
	hash := t.Hasher(key, uintptr(h.hash0))
    // 标记当前goroutine正在写map
	h.flags ^= hashWriting

	if h.buckets == nil {
		h.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)
	}

again:
	bucket := hash & bucketMask(h.B) // 根据key的hash值对buckets数组长度取模找到key对应的bucket 这里用&运算代替取模操作
	if h.growing() { // 如果当前正在扩容
		growWork(t, h, bucket) // 下面解释
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize))) // 找到key对应的bucket
	top := tophash(hash) // 获取key的hash值的前8位

	var inserti *uint8 // 插入的索引
	var insertk unsafe.Pointer // 插入的key
	var elem unsafe.Pointer // 插入的val
bucketloop:
	for { // 外层遍历bucket以及bucket内的overflow
		for i := uintptr(0); i < bucketCnt; i++ { // 内层循环遍历bucket的8个cell
			if b.tophash[i] != top {
				if isEmpty(b.tophash[i]) && inserti == nil { // 记录第一个空的插槽,如果遍历完当前bucket以及overflow之后没有找到对应的key,就把值插入这个位置
					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.ValueSize))
				}
				if b.tophash[i] == emptyRest { // 这个emptyRest表示i后面的所有cell都是kong不用遍历了
					break bucketloop
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			if t.IndirectKey() {
				k = *((*unsafe.Pointer)(k))
			}
			if !t.Key.Equal(key, k) { // 前8位hash相等之后接着在比较一下key
				continue
			}
			// already have a mapping for key. Update it.
			if t.NeedKeyUpdate() { // 如果找到了对应的key,就执行更新操作
				typedmemmove(t.Key, k, key)
			}
			elem = 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
	}

	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { // 如果当前没有扩容,并且key的数量太多或者overflow数量太多,就进行扩容
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again
	}

	if inserti == nil { // 到此如果inserti为nil表示上面全部遍历完还没找到对应的key并且也没有空的位置 所有就需要创建overflow
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.KeySize))
	}

	// store new key/elem at insert position
	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) // 插入key
	*inserti = top
	h.count++

done:
	if h.flags&hashWriting == 0 { //再次检查有没有别的goroutine在写map
		fatal("concurrent map writes")
	}
	h.flags &^= hashWriting // 解除对此map的写状态
	if t.IndirectElem() {
		elem = *((*unsafe.Pointer)(elem))
	}
    // val的放入是用汇编写的
	return elem
}
2.hashGrow() 

主要就是判断是需要进行增量扩容还是等量扩容,然后创建新buckets

func hashGrow(t *maptype, h *hmap) {
    // 默认进行增量扩容
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) { // 如果不是因为装载因子超过阈值而触发的扩容,也就是进行等量扩容
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	// commit the grow (atomic wrt gc)
	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 {
		// Promote current overflow buckets to the old generation.
		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
	}

	// the actual copying of the hash table data is done incrementally
	// by growWork() and evacuate().
}
3.growWork()

扩容前夕,主要的搬运数据函数是evacuate(),这里的第一次搬运的数据是确认搬迁老的 bucket 对应正在使用的 bucket。第二次是搬运老桶数组中还没搬运的索引最小的哪个bucket里面的数据。

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// make sure we evacuate the oldbucket corresponding
	// to the bucket we're about to use
	evacuate(t, h, bucket&h.oldbucketmask())

	// evacuate one more oldbucket to make progress on growing
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}
4.evacuate()

这里的xy的x表示新buckets的前一半,y就是后一半

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))
	newbit := h.noldbuckets() // 老桶数组的长度
	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))
		}

		for ; b != nil; b = b.overflow(t) { // 外层遍历bucket以及bucket内的overflow
			k := add(unsafe.Pointer(b), dataOffset)
			e := add(k, bucketCnt*uintptr(t.KeySize))
			for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.KeySize)), add(e, uintptr(t.ValueSize)) { // 遍历bucket内的cell
				top := b.tophash[i]
				if isEmpty(top) { // 标记当前key已经被搬移
					b.tophash[i] = evacuatedEmpty
					continue
				}
				if top < minTopHash {
					throw("bad map state")
				}
				k2 := k
				if t.IndirectKey() {
					k2 = *((*unsafe.Pointer)(k2))
				}
				var useY uint8 // 默认是0
				if !h.sameSizeGrow() { // 如果不是等量扩容
					hash := t.Hasher(k2, uintptr(h.hash0))
                    // 这里的特例是相同的key两次算出的hash值不一样,只有在float变量的Nan()出现
					if h.flags&iterator != 0 && !t.ReflexiveKey() && !t.Key.Equal(k2, k2) {
						useY = top & 1
						top = tophash(hash)
					} else {
						if hash&newbit != 0 { // 这里就是hash % 新桶的长度 只不过是换成&运算
							useY = 1
						}
					}
				}

				if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
					throw("bad evacuatedN")
				}
                // 表示数据搬移到Y
				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
				dst := &xy[useY]                 // evacuation destination

				if dst.i == bucketCnt { // 如果当前cell溢出,申请一个溢出桶
					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 // 更新top hash值
				if t.IndirectKey() {
					*(*unsafe.Pointer)(dst.k) = k2 // 更新key指针
				} else {
					typedmemmove(t.Key, dst.k, k) // 更新key
				}
				if t.IndirectElem() {
					*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e) // 更新值指针
				} else {
					typedmemmove(t.Elem, dst.e, e) // 更新值
				}
				dst.i++ // 接着搬移下一个cell
				dst.k = add(dst.k, uintptr(t.KeySize))
				dst.e = add(dst.e, uintptr(t.ValueSize))
			}
		}
		// 如果老的桶已经搬迁完没有goroutine使用让gc回收
		if h.flags&oldIterator == 0 && t.Bucket.PtrBytes != 0 {
			b := add(h.oldbuckets, oldbucket*uintptr(t.BucketSize))
			ptr := add(b, dataOffset)
			n := uintptr(t.BucketSize) - dataOffset
			memclrHasPointers(ptr, n)
		}
	}
    // 更新搬迁进度
	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}
5.advanceEvacuationMark()
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
	h.nevacuate++
	stop := h.nevacuate + 1024
	if stop > newbit {
		stop = newbit
	}
	for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
		h.nevacuate++
	}
	if h.nevacuate == newbit { // 全部数据都迁移完成
		h.oldbuckets = nil // 清除老的buckets
		if h.extra != nil {
			h.extra.oldoverflow = nil
		}
		h.flags &^= sameSizeGrow
	}
}

八.删除过程

1.mapdelete()

其实就是先看看是否在扩容,如果是就帮忙扩容,然后接着遍历bucket寻找要删除的key,找到就删除,然后就是判断是否需要给cell标记emptyRest

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	if h == nil || h.count == 0 {
		if t.HashMightPanic() {
			t.Hasher(key, 0) // see issue 23734
		}
		return
	}
	if h.flags&hashWriting != 0 {
		fatal("concurrent map writes")
	}

	hash := t.Hasher(key, uintptr(h.hash0))

	h.flags ^= hashWriting

	bucket := hash & bucketMask(h.B)
	if h.growing() {
		growWork(t, h, bucket)
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
	bOrig := b
	top := tophash(hash)
search:
	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 search
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			k2 := k
			if t.IndirectKey() {
				k2 = *((*unsafe.Pointer)(k2))
			}
			if !t.Key.Equal(key, k2) {
				continue
			}
            // 删除key
			if t.IndirectKey() {
				*(*unsafe.Pointer)(k) = nil
			} else if t.Key.PtrBytes != 0 {
				memclrHasPointers(k, t.Key.Size_)
			}
			e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
            // 删除value
			if t.IndirectElem() {
				*(*unsafe.Pointer)(e) = nil
			} else if t.Elem.PtrBytes != 0 {
				memclrHasPointers(e, t.Elem.Size_)
			} else {
				memclrNoHeapPointers(e, t.Elem.Size_)
			}
			b.tophash[i] = emptyOne
			if i == bucketCnt-1 { // 如果i是最后一个
				if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest { // 并且下一个overflow不为空和下一个overflow的第一个cell不为emptyRest 
					goto notLast
				}
			} else {
				if b.tophash[i+1] != emptyRest { // 如果当前删除的cell后面的cell不为空
					goto notLast
				}
			}
            // 到此表明删除的cell后面一个cell是emptyRest,所以需要往前遍历,把为空的cell置为emptyRest
			for { // 从i开始往前找,如果cell为空就置为emptyRest
				b.tophash[i] = emptyRest
				if i == 0 {
					if b == bOrig {
						break // beginning of initial bucket, we're done.
					}
					// Find previous bucket, continue at its last entry.
					c := b
					for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
					}
					i = bucketCnt - 1
				} else {
					i--
				}
				if b.tophash[i] != emptyOne {
					break
				}
			}
		notLast: // 正常删除结束
			h.count-- // 键值对数量减一
			if h.count == 0 {
				h.hash0 = fastrand()
			}
			break search
		}
	}

	if h.flags&hashWriting == 0 {
		fatal("concurrent map writes")
	}
	h.flags &^= hashWriting
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/673198.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Educational Codeforces Round 166 (Rated for Div. 2) (A~C)

A. Verify Password 思路:按照ASCLL值进行比较就行(因为字母的ASCLL本来就在数字后面),所以,只要找到前面比后面的数大就输出NO,反之YES 代码实现: #include<bits/stdc.h> using namespace std; #define N 100005 typedef long long ll; ll n, m, num, sum, t; ll a[N]…

通过f-string编写简洁高效的Python格式化输出代码

Python 3.6中引入的f-string是Python中最常用的特征之一&#xff0c;它可以让我们编写更干净、更高效和更易于维护的代码&#xff0c;我们今天就由浅入深来详细介绍使用它的一些技巧。 对齐文本 在格式化输出时&#xff0c;对齐对可读性至关重要。无论是生成报告、记录数据还是…

kibana7.17.0查看index

kibana查看index Kibana 提供了一个直观的用户界面&#xff0c;可以查看和管理 Elasticsearch 的索引。以下是如何在 Kibana 中查看 Elasticsearch 索引的步骤&#xff1a; 1. 打开 Kibana 首先&#xff0c;确保 Kibana 已经启动并可以访问。通常&#xff0c;你可以通过浏览…

开箱即用的Spring Boot 企业级开发平台【毕设项目推荐】

项目概述 基于 Spring 实现的通用权限管理平台&#xff08;RBAC模式&#xff09;。整合最新技术高效快速开发&#xff0c;前后端分离模式&#xff0c;开箱即用。 核心模块包括&#xff1a;用户、角色、职位、组织机构、菜单、字典、日志、多应用管理、文件管理、定时任务等功能…

【自己动手】自制刷题系统(php+layui应用 社区工作者题库)

现在各种证都可以考&#xff0c;网上免费刷题的APP一大堆&#xff0c;我自己也想搞一个。网上的刷题软件有的用的很舒服&#xff0c;有的体检就很不好&#xff0c;热门的考试基本都有&#xff0c;不热门的基本就很差了&#xff0c;网上也有提供自制试卷的APP&#xff0c;但都有…

弹性云服务器ECS

ECS的功能&#xff1a; 1、类型丰富。支持多规格类型、多镜像类型、多磁盘种类。 2、灵活的计费模式。支持包年包月或按需计费模式购买云服务器&#xff0c;满足不同应用场景&#xff0c;根据业务波动随时购买或释放资源。 3、数据可靠。基于分布式架构的&#xff0c;可弹性…

2024年6月2日 (周日) 叶子游戏新闻

中医百科中药: 中医百科中药是一款非常强大的中药知识科普软件&#xff0c;该应用提供500多味中草药的文献资料&#xff0c;强大的搜索功能可根据功效、特点和关键词来快速查找中药&#xff0c;而且每味中药的图片、功效、主治、炮制方法等百科知识&#xff0c;可以很好的帮助你…

Opera 浏览器与Google联手,推出由Gemini驱动的全新AI功能

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

MedSegDiff-V2: Diffusion-Based Medical Image Segmentation with Transformer 论文总结

标题&#xff1a;MedSegDiff-V2: Diffusion-Based&#xff08;基于扩散模型&#xff09;Medical Image Segmentation&#xff08;医学图像分割&#xff09;with Transformer 论文&#xff08;AAAI&#xff09;&#xff1a;https://ojs.aaai.org/index.php/AAAI/article/view/28…

2024-6-2 石群电路-21

2024-6-2&#xff0c;星期日&#xff0c;天气&#xff1a;阴&#xff0c;心情&#xff1a;晴。今天没什么特别的事情发生&#xff0c;心情还是一如既往的好&#xff0c;明天就周一啦&#xff0c;虽然我暂时不用上班&#xff0c;但是希望大家新的一周元气满满~ 今日观看了石群老…

ANAH数据集- 大模型幻觉细粒度评估工具

大型语言模型&#xff08;LLMs&#xff09;在各种自然语言处理任务中取得了显著的性能提升。然而&#xff0c;它们在回答用户问题时仍面临一个令人担忧的问题&#xff0c;即幻觉&#xff0c;它们会产生听起来合理但不符合事实或无意义的信息&#xff0c;尤其是当问题需要大量知…

MongoDB-4.2.1 之安装和使用

安装 下载安装包 我自己电脑是 Windows7 的老古董&#xff0c;所以就下载老版本的 MongoDB。 mongodb: https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2012plus-4.2.1.zip 解压安装包到指定路径 我解压到的 C 盘 C:\mongodb-4.2.1 添加环境变量 创建数据库和…

gitlab服务器迁移(亲测有效)

描述&#xff1a;最近公司迁移gitlab&#xff0c;我没有迁移过&#xff0c;经过网上查找资料最终完成迁移&#xff0c;途中也遇到挺多坑和两个问题&#xff0c;希望能帮到你。 新服务器安装gitlab 注意&#xff1a;新服务器gitlab版本也需要和旧版本一致。 首先查看原Gitlab…

渗透测试靶机----FirstLeads_1.3

渗透测试靶机----FirstLeads_1.3 启动靶机&#xff0c;扫描ip&#xff0c;平平无奇 可以看出&#xff0c;这里是存在139这个主机的&#xff0c;这就是扫描出来的靶机ip继续探测端口及其他信息 发现这里只开启了80 端口 尝试一些基本目录&#xff0c;发现存在robot.txt文件目录…

WIN10环境下xposed环境搭建

禁止拿来干坏事&#xff0c;仅做学习为目的 环境需求 1.夜神模拟器7.1 2.Android stdio 2022.3.1 3. Adb环境配置 具体实现 1.安装xposed 打开可一键安装&#xff0c;重启 2.连接虚拟机 adb connect 127.0.0.1:620013.打开as,进入project 4.在lib下添加准备好的jar包 …

JAVAEE1

Web前端&#xff1a; 1.建立web开发的息维模式写代码不仅仅是为了实现某个功能&#xff0c;更是学习解决问题的思维方式 2.先使用&#xff0c;再理解&#xff0c;会导致刚开始比较懵&#xff0c;不知其所以然.切忌不可深陷其中&#xff0c; 3.涉及简单的软件工程的设计思想&…

排序方法大汇总

以下所有排序方法均以排升序为例 一.插入排序 1.直接插入排序 1>方法介绍&#xff1a;假定前n个数据有序&#xff0c;将第n1个数据依次与前n个数据相比&#xff0c;若比第i个数据小且比第i-1个数据大则插入两者之间 2>时间复杂度&#xff1a;O(N^2) 空间复杂度&#…

数据结构与算法05-链表

介绍 基于结点的数据结构拥有独特的存取方式&#xff0c;因此在某些时候具有性能上的优势。 本章我们会探讨链表&#xff0c;它是最简单的一种基于结点的数据结构&#xff0c;而且也是后续内容的基础。 你会发现&#xff0c;虽然链表和数组看上去差不多&#xff0c;但在性能上…

ThingsBoard MQTT 连接认证过程 源码分析+图例

整个连接过程如图所示&#xff1a; 高清图片链接 1、环境准备 thingsboard3.5.1 源码启动。&#xff08;不懂怎么启动的&#xff0c;大家可以看我的博文ThingsBoard3.5.1源码启动&#xff09;MQTTX 客户端&#xff08;用来连接 thingsboard MQTT&#xff09;默认配置。queue.…

接口以及会话控制

接口 接口是前后端交互通信的桥梁 简单理解&#xff1a;一个接口就是服务中的一个路由规划&#xff0c;根据请求响应结果 接口的英文单词是API&#xff08;Application Program Interface),所以有时也称之为API接口 这里的接口指的是数据接口&#xff0c;与编程语言&#x…