go语言之map
介绍
golang中的map是常用的数据结构,又称为hash表。接下来根据go源码,简单介绍一下go的map。
map结构
在go中实例化,关键字是map。但是在go中,会对go中是的hmap。
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
先把接下来的各个字段做一下简单的说明。
hmap
count
这个是map是有多少k/v键值对。
flags
这个是map当前是处于什么状态,主要是有
iterator = 1 // there may be an iterator using buckets 循环当前buckets
oldIterator = 2 // there may be an iterator using oldbuckets // 循环老的buckets
hashWriting = 4 // a goroutine is writing to the map //在写
sameSizeGrow = 8 // the current map growth is to a new map of the same size // map在扩容
B
有多少buckets,注意是2的次方。buckets是真正装载key/val的地方。
noverflow
溢出桶的数量。注意是这个预估,并不是准确的值,当B的值小于16的时候是准确的,大于16的时候,是有概率的,见下图:
hash0
生成的随机数,用来生成key等。
buckets
类型是unsafe.Pointer。存放的buckets的地址的指针。
oldbuckets
类型是unsafe.Pointer。存放的是老的buckets的地址的指针,针对是老的buckets,针对扩容的时候。
nevacuate
扩容进度计数器。少于这个数字的buckets都会被回收。
mapextra
overflow
存放的所有溢出桶的地址。这个是为了保存所有溢出桶的存活,不被gc回收,因此用指针进行存放
oldoverflow
存放的所有老的溢出桶的地址。这个是针对扩容的时候,老的通上的溢出桶。这个是为了保存所有溢出桶的存活,不被gc回收,因此用指针进行存放
nextOverflow
指向的下一个溢出桶的地址。
上面是hmap的结构体,再看一下桶的结构体。
type bmap struct {
tophash [bucketCnt]uint8
}
//编译期间展开如下
type bmap struct{
topbits [8]uint8 //用于表示标志位或hash值高八位来快速定位K/V位置
keys [8]keytype
value [8]valuetype
pad uintptr //此字段go1.16.2版本已删除
overflow uintptr //连接下个bmap溢出桶
}
所以这个bmap就是桶的大小。
topbits = 8字节
keys = 8*8=64字节
value = 8 * 8 = 64字节
overflow(64位) = 8字节
所以总共是=8+64+64+8 = 144字节
这个也可以通过断点看出。首先下面是go代码。
package main
func main() {
m := make(map[int]int, 10)
m[1] = 1
}
然后通过dlv断点打出makemap中的maptype
可以看出bucketsize就是144字节。
初始化
就是创建map,除了特别的初始化,调用的都是runtime下面的makemap方法,接下来根据go的源码看一下实现。
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
// t是map中的元素类型
// hint是就是make传的容量
// 根据hint和t.bucket.size判断申请内存是否超过限制
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的数量
// 如果hint大于8并且hint大于(1<<b)*6.5 就每次增长1,直接不满足
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
// If hint is large zeroing this memory could take a while.
// 如果是make(map[int]int)
if h.B != 0 {
var nextOverflow *bmap
// 初始化buckets和溢出桶
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
// makeBucketArray initializes a backing array for map buckets.
// 1<<b is the minimum number of buckets to allocate.
// dirtyalloc should either be nil or a bucket array previously
// allocated by makeBucketArray with the same t and b parameters.
// If dirtyalloc is nil a new backing array will be alloced and
// otherwise dirtyalloc will be cleared and reused as backing array.
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
// 得到 1 << b 个buckets
base := bucketShift(b)
nbuckets := base
// For small b, overflow buckets are unlikely.
// Avoid the overhead of the calculation.
// 如果大于4就需要分配溢出桶
if b >= 4 {
// Add on the estimated number of overflow buckets
// required to insert the median number of elements
// used with this value of b.
// 增加额外的溢出桶1的数量
nbuckets += bucketShift(b - 4)
// 需要分配的内存大小
sz := t.bucket.size * nbuckets
// 获取的内存向上取整得到内存
up := roundupsize(sz)
// 不一致生成新的buckets数量
if up != sz {
nbuckets = up / t.bucket.size
}
}
if dirtyalloc == nil {
// 申请新的内存数组
buckets = newarray(t.bucket, int(nbuckets))
} else {
// dirtyalloc was previously generated by
// the above newarray(t.bucket, int(nbuckets))
// but may not be empty.
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.ptrdata != 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
if base != nbuckets {
// We preallocated some overflow buckets.
// To keep the overhead of tracking these overflow buckets to a minimum,
// we use the convention that if a preallocated overflow bucket's overflow
// pointer is nil, then there are more available by bumping the pointer.
// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
// 指针移动到下一个溢出桶
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
// 指向最后一个buckets
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
// 将最后一个buckets的overflow指向第一个buckets
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}
添加值
这里的添加值就是在在hamp中添加值,代码如下
package main
import "fmt"
func main() {
m := make(map[int]int, 10)
m[1] = 1
}
还是一样通过反编译看出底层的调用是runtime.mapassign_fast64。接下来根据go的源码简单说一下mapassign_fast64这个方法。
func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
// 判断是否为nil
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
// 判断竞争条件
if raceenabled {
callerpc := getcallerpc()
racewritepc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapassign_fast64))
}
// 判断是否并发写
if h.flags&hashWriting != 0 {
fatal("concurrent map writes")
}
// 感觉key和随机种子生成hash的key
hash := t.hasher(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
// h.flags 增加writing标志位
h.flags ^= hashWriting
// 如果bucket不存在,那么生成
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 找到这个hash对应的桶
bucket := hash & bucketMask(h.B)
// 这个map是否在增长
if h.growing() {
growWork_fast64(t, h, bucket)
}
// 移动指针找到对应的buckets
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
// 初始化变量
var insertb *bmap
var inserti uintptr
var insertk unsafe.Pointer
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
// 判断是否为空 如果为空说明可以使用
// 判断b.tophash[i] 是否可用 这个为空和被删除等都是true
// 因为删除的时候会设置为1 为空是0
if isEmpty(b.tophash[i]) {
// 判断是否为nil
if insertb == nil {
insertb = b
inserti = i
}
// 说明为空 直接跳出循环
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 如果可以用 得到这个key上的指针对应的值
k := *((*uint64)(add(unsafe.Pointer(b), dataOffset+i*8)))
if k != key {
continue
}
insertb = b
inserti = i
goto done
}
// 找到key后,判断是否有溢出桶,如果有溢出桶,那么使用溢出桶
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 当出生以下情况 进行扩容
// 没有处在扩容的状态
// 超过了负载因子 key数量大于 1<<B *6.5或者有过多的溢出桶 大于 1 << (h.B&15)
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
// 进行扩容
hashGrow(t, h)
goto again
}
// 如果insertb为nil
if insertb == nil {
// 生成一个溢出桶 并且将当前的buckets 关联过去
insertb = h.newoverflow(t, b)
inserti = 0 // not necessary, but avoids needlessly spilling inserti
}
// 将inserti的值放到tophash中
insertb.tophash[inserti&(bucketCnt-1)] = tophash(hash) // mask inserti to avoid bounds checks
// 找到桶的指针
insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*8)
// 将值存入
*(*uint64)(insertk) = key
// 数量加一
h.count++
done:
// 找到value所在的指针
elem := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*8+inserti*uintptr(t.elemsize))
if h.flags&hashWriting == 0 {
fatal("concurrent map writes")
}
// 将hashWriting从h.flags去掉
h.flags &^= hashWriting
// 返回
return elem
}
这里不扩容版本的就说完了,这里只是把elem所在的指针返回,并没有进行赋值,看一下编译的代码
从汇编来看,最后通过汇编指令MOVD实现。
上面的流程是没有发生扩容,接下来看看扩容的流程。
扩容
首先是判断是否需要进行扩容,也就是hashGrow这个方法
func hashGrow(t *maptype, h *hmap) {
// 默认是扩容一倍
bigger := uint8(1)
// 说明溢出桶过多了 会导致找到一个key的时间增加
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标志位
flags := h.flags &^ (iterator | oldIterator)
// 如果iterator存在,认为是旧的,并放到flags
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 {
// 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
}
// 如果nextOverflow 存在 赋值给下一个
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().
接下来就是搬迁的过程,go中的搬迁不是一次完成了,而且每次赋值的时候搬迁一些。
首先是判断是否在扩容,在mapassign_fast64中:
func growWork_fast64(t *maptype, h *hmap, bucket uintptr) {
// bucket&h.oldbucketmask() 得到老的buckets所在的位置
// 开始搬迁
evacuate_fast64(t, h, bucket&h.oldbucketmask())
// 如果没有搬迁完 那么在搬迁一次
if h.growing() {
evacuate_fast64(t, h, h.nevacuate)
}
}
然后看一下真正的搬迁代码:
func evacuate_fast64(t *maptype, h *hmap, oldbucket uintptr) {
// 获取需要搬迁的旧桶
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 旧桶的buckets数量
newbit := h.noldbuckets()
// 是否需要搬迁 通过桶的tophash的第一位判断
if !evacuated(b) {
// 申明两个搬迁的桶
// evacDst is an evacuation destination.
/*type evacDst struct {
b *bmap // current destination bucket
i int // key/elem index into b
k unsafe.Pointer // pointer to current key storage
e unsafe.Pointer // pointer to current elem storage
}*/
var xy [2]evacDst
// 第一个搬迁的buckets 注意这里是oldbucket的位置,但是用的是h.buckets 而不是h.oldbuckets。所以是需要迁移的地址。
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*8)
// 如果发生了扩容 再申明一个buckets
if !h.sameSizeGrow() {
// 注意是oldbucket+newbit 是在x的基础上增加了newbit
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*8)
}
// 依次循环 当前bucktes和对应的溢出桶
for ; b != nil; b = b.overflow(t) {
// 通过指针偏移 找到key的地址和value的地址
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*8)
// 依次循环每个tophash 还是通过指针找到key和value
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, 8), add(e, uintptr(t.elemsize)) {
// 获取每个tophash 并且判断状态
top := b.tophash[i]
if isEmpty(top) {
b.tophash[i] = evacuatedEmpty
continue
}
if top < minTopHash {
throw("bad map state")
}
var useY uint8
// 如果扩容
if !h.sameSizeGrow() {
// 获取hash值
hash := t.hasher(k, uintptr(h.hash0))
// 如果在老的命中了 存到新的桶中
if hash&newbit != 0 {
useY = 1
}
}
// 存tophash的值 将老的tophash值设置成搬迁的状态
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY, enforced in makemap
// 获取需要搬迁的地址
dst := &xy[useY] // evacuation destination
// 如果是已经是最后一个tophash了,说明需要溢出桶
// 创建溢出桶
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*8)
}
// 将top值复制过去
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
// 复制key
if t.key.ptrdata != 0 && writeBarrier.enabled {
if goarch.PtrSize == 8 {
// Write with a write barrier.
*(*unsafe.Pointer)(dst.k) = *(*unsafe.Pointer)(k)
} else {
// There are three ways to squeeze at least one 32 bit pointer into 64 bits.
// Give up and call typedmemmove.
typedmemmove(t.key, dst.k, k)
}
} else {
*(*uint64)(dst.k) = *(*uint64)(k)
}
// 复制value
typedmemmove(t.elem, dst.e, e)
// 移动dst的i
dst.i++
// 移动key和value的指针
dst.k = add(dst.k, 8)
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// 清除老的bucktes 是当前buckets
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// Preserve b.tophash because the evacuation
// state is maintained there.
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
// 如果迁移结束了 那么把h.oldbuckets和h.extra.oldoverflow设置为nil
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
如果已经搬迁完成,那么接下来再搬迁一次,注意调用的是h.nevacuate,也就是说肯定会走到advanceEvacuationMark这个方法中。
查询
map中的查询就是通过key查询map中的值,也就是如下的代码
package main
func main() {
m := make(map[int]int, 10)
_ = m[1]
}
通过汇编可以看出获取的底层调用是mapaccess1_fast64。其实是可以添加值的逻辑是类似的,接下来根据代码的逻辑梳理一遍。
func mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
// 竞性条件判断
if raceenabled && h != nil {
callerpc := getcallerpc()
racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapaccess1_fast64))
}
// 如果h为nil或者没有数量返回默认的
if h == nil || h.count == 0 {
return unsafe.Pointer(&zeroVal[0])
}
// 判断当前是否有写操作 如果有直接panic 注意这个panic是无法被捕获
if h.flags&hashWriting != 0 {
fatal("concurrent map read and map write")
}
// 初始化
var b *bmap
// 如果为0 说明就只有一个buckets
if h.B == 0 {
// One-bucket table. No need to hash.
b = (*bmap)(h.buckets)
} else {
// 生成hash值
hash := t.hasher(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
// 获取buckets的数量 也就是 1 << h.B
m := bucketMask(h.B)
// 指针偏移找到当前的bmap
b = (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 如果存在老的buckets 先判断老的
if c := h.oldbuckets; c != nil {
// 如果存在扩容 将m向右边偏移一位 也就是除2
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
// 获取老的buckets所在的位置
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
// 判断是否已经搬迁完成 如果没有 那么就用老的bucktes
if !evacuated(oldb) {
b = oldb
}
}
}
// 循环判断当前的buckets和溢出桶
for ; b != nil; b = b.overflow(t) {
// 指针偏移找到key
for i, k := uintptr(0), b.keys(); i < bucketCnt; i, k = i+1, add(k, 8) {
// 判断key是否一致 并且tophash存在 一致
// 那么通过指针偏移找到对应的值
if *(*uint64)(k) == key && !isEmpty(b.tophash[i]) {
return add(unsafe.Pointer(b), dataOffset+bucketCnt*8+i*uintptr(t.elemsize))
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
相比对添加值,这个查询值还是比较简单的
删除key
删除key的底层方法是delete,代码如下
package main
func main() {
m := make(map[int]int, 10)
delete(m, 1)
}
一样还是通过反编译,可以看出来底层的调用是mapdelete_fast64的方法。然后看一下这个方法的实现。
func mapdelete_fast64(t *maptype, h *hmap, key uint64) {
// 判断竞争条件和map是否为nil
if raceenabled && h != nil {
callerpc := getcallerpc()
racewritepc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapdelete_fast64))
}
if h == nil || h.count == 0 {
return
}
// 如果有写那么抛出异常
if h.flags&hashWriting != 0 {
fatal("concurrent map writes")
}
// 根据key生成hash值
hash := t.hasher(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
// 给h.flags增加hashWriting这个标志位
h.flags ^= hashWriting
// 获取当前的bucket
bucket := hash & bucketMask(h.B)
// 判断是否有扩容 如果有扩容 去搬迁见添加值
if h.growing() {
growWork_fast64(t, h, bucket)
}
// 得到这个key对应的bucket
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
// 得到一个临时的bucket
bOrig := b
search:
// 依次判断当前bucket和对应的溢出桶
for ; b != nil; b = b.overflow(t) {
// 通过指针偏移,得到当前的key.因为是在64位平台,所以移动8字节
for i, k := uintptr(0), b.keys(); i < bucketCnt; i, k = i+1, add(k, 8) {
// 如果key一致并且tophash的非空
if key != *(*uint64)(k) || isEmpty(b.tophash[i]) {
continue
}
// 当key是指针的时候进行清空
if t.key.ptrdata != 0 {
if goarch.PtrSize == 8 {
*(*unsafe.Pointer)(k) = nil
} else {
// There are three ways to squeeze at one ore more 32 bit pointers into 64 bits.
// Just call memclrHasPointers instead of trying to handle all cases here.
memclrHasPointers(k, 8)
}
}
// 根据当前的bucket,和key找到对应的value
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*8+i*uintptr(t.elemsize))
// 清楚当前value
if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
// 设置成emptyOne 注意没有设置成emptyRest
// 是因为设置成emptyOne可以后面重复利用
// 如果不能重复利用 接下来会设置成emptyRest
b.tophash[i] = emptyOne
// 如果是最后一个
if i == bucketCnt-1 {
// 并且当前的bucket对应的溢出桶有在使用
// 那么说明当前的bucket是满员,那么直接跳过
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
// 如果下一个tophash也是可以用,说明当前bucket是不需要进行回收,也跳过
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
// 说明有需要回收的
for {
// 直接把当前的值设置成emptyRest
b.tophash[i] = emptyRest
// 因为是--,所以当为0的时候 判断是否是溢出桶
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// 得到前面一个溢出桶 然后依次判断
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1
} else {
i--
}
// 不为emptyOne 说明是有值 那么直接跳出循环
if b.tophash[i] != emptyOne {
break
}
}
notLast:
// 把数量减一
h.count--
// Reset the hash seed to make it more difficult for attackers to
// repeatedly trigger hash collisions. See issue 25237.
if h.count == 0 {
h.hash0 = fastrand()
}
break search
}
}
// 再判断一次
if h.flags&hashWriting == 0 {
fatal("concurrent map writes")
}
// 将hashWriting 这个flag从h.flags中去掉
h.flags &^= hashWriting
}