Golong

76 阅读6分钟

Go Map底层实现

Go语言的map是用哈希表 + 拉链法来实现的

// Go map 的底层结构体表示
type hmap struct {
    count     int    // map中键值对的个数,使用len()可以获取 
    flags     uint8
    B         uint8  // 哈希桶的数量的log2,比如有8个桶,那么B=3
    noverflow uint16 // 溢出桶的数量
    hash0     uint32 // 哈希种子

    buckets    unsafe.Pointer // 指向哈希桶数组的指针,数量为 2^B 
    oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针,当扩容时不为nil 
    nevacuate  uintptr        

    extra *mapextra  // 可选字段
}

const (
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits     // 桶数量 1 << 3 = 8
)

// Go map 的一个哈希桶,一个桶最多存放8个键值对
type bmap struct {
    // tophash存放了哈希值的最高字节
    tophash [bucketCnt]uint8

    // 在这里有几个其它的字段没有显示出来,因为k-v的数量类型是不确定的,编译的时候才会确定
    // keys: 是一个数组,大小为bucketCnt=8,存放Key
    // elems: 是一个数组,大小为bucketCnt=8,存放Value
    // 你可能会想到为什么不用空接口,空接口可以保存任意类型。但是空接口底层也是个结构体,中间隔了一层。因此在这里没有使用空接口。
    // 注意:之所以将所有key存放在一个数组,将value存放在一个数组,而不是键值对的形式,是为了消除例如map[int64]所需的填充整数8(内存对齐)

    // overflow: 是一个指针,指向溢出桶,当该桶不够用时,就会使用溢出桶
}

image.png

image.png

  • map的初始化

makemap_small: 我们可以看到,它直接new了一个hmap,然后返回hmap的指针。与Go中的切片不同,切片是返回一个结构体,而map返回的是结构体的指针。

makemap: new了一个hmap的结构体,然后根据传入的数据的数量来算出需要的B的数量。然后进行哈希桶的内存分配,它还会多创建一些溢出桶,extra结构体的nextoverflow字段保存了这些溢出桶,最后返回hmap的指针。

makeBucketArray: 创建哈希桶数组,根据b来计算数组的大小,如果b>=4,还会创建一些溢出桶,溢出桶的数量为 1 << (b - 4)。计算完后,创建数组,然后返回普通桶和溢出桶的地址。

在创建map的时候,首先要确定B,假设B为3,那么桶的数量就为2^3=8,其次,如果B >= 4,也会创建一些溢出桶。然后创建mapextra类型的结构体,其中的nextoverflow保存了下一个可用的溢出桶的地址。

假设位于一号槽的桶已经用满了,那么就会使用extra字段来寻找一个新的可用的溢出桶,然后使用bmap中的overflow字段指向溢出桶来组成一个链表。

// makemap_small 实现了Go map的构造
// make(map[k]v, hint) 当hint最多为bucketCnt=8时,就会使用该函数来构造map
func makemap_small() *hmap {
    // 就是new了一个hmp的结构体,随机生成hash0,然后返回它的指针
    h := new(hmap)
    h.hash0 = fastrand()
    return h
}


func makemap(t *maptype, hint int, h *hmap) *hmap {
    ...

    // 构造hmap结构体
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()

    // 根据传入的数据的数量来算出需要的B的大小
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B

    // 分配哈希桶的内存空间
    // 如果B为0,那么将会进行延迟分配
    if h.B != 0 {
        var nextOverflow *bmap
        /* 
        创建哈希桶以及一些溢出桶,假设创建了8个桶,那么只能存放 8 * 8 = 64对键值,如果再存放的话,就会溢出,因此会多创建一些溢出桶
        这些溢出桶会存放在extra字段中
        */
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
        // 如果有溢出桶,就初始化extra字段,保存下一个可用溢出桶的地址
        if nextOverflow != nil {
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }

    return h
}


type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap

    // 保存了下一个可用的溢出桶的地址
    nextOverflow *bmap
}

// 创建哈希桶数组
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
    // 计算数组大小   base := 1 << b
    base := bucketShift(b)
    nbuckets := base
    // 当b >= 4 时会创建溢出桶,溢出桶的数量为 1 << (b - 4)
    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 {
        // 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 {
        nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
        last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
        last.setoverflow(t, (*bmap)(buckets))
    }
    // 返回普通桶和溢出桶的地址
    return buckets, nextOverflow
}

  • map的访问

map的访问,首先要获取桶号,然后循环匹配该桶和溢出桶中的tophash的值,每个桶中的tophash没有保存哈希值的全部,而是保存了高八位,是为了快速遍历。匹配成功,还要验证key值是否相等,如果相等就说明找到了。

image.png

// mapaccess1 返回一个指针,这个指针不会为nil,如果key不存在,则返回该值对应的0值
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {

    ...  // 省略了相关代码

    // 如果h为nil 或者数量为0,如果为nil说明map没有初始化,可能会panic
    if h == nil || h.count == 0 {
        if t.hashMightPanic() {
            t.hasher(key, 0) // see issue 23734
        }
        // 返回对应的0值
        return unsafe.Pointer(&zeroVal[0])
    }
    // 防止map并发读写,检测到并发读写就panic
    if h.flags&hashWriting != 0 { // 为1表示有并发写
        throw("concurrent map read and map write")
    }
    // 对key和hash0进行哈希,计算哈希值
    hash := t.hasher(key, uintptr(h.hash0))
    // 计算桶掩码m,m & hash即可得到桶号
    // 假设 B=3,那么 m = 1 << 3 - 1 = 0b1000 - 0b1 = 0b111, 此时 m & hash即可得到hash的后三位,即为桶号
    m := bucketMask(h.B)
    // 根据桶号获取bmap类型的哈希桶
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
    // 如何h.oldbuckets != nil,说明此时map正在扩容,需要判断当前要访问的key是在新桶中还是在旧桶中
    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)))
        // 判断数据是否被驱逐了,如果没有被驱逐,应该在旧桶中查找
        // 这个函数的逻辑很简单,就是判断tophash[0]是否在大于1且小于5,因为被驱逐的桶的tophash被置为4
        if !evacuated(oldb) {
            b = oldb
        }
    }
    // 计算tophash,也就是hash的高八位
    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
            }
            // 匹配tophash成功后,根据在tophash中的偏移,计算key的地址
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            // 比较是否是要找的key
            if t.key.equal(key, k) {
                // 如果是,则计算val的地址
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                        e = *((*unsafe.Pointer)(e))
                }
                // 返回val
                return e
            }
        }
    }
    // 没有找到,返回对应的0值
    return unsafe.Pointer(&zeroVal[0])
}

// mapaccess2与mapaccess逻辑相同,只是多了个bool的返回值,如果没有找到就为false,否则为true
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {

    ...
    
}
  • map的插入

map的插入首先要查找要插入的key是否已经存在,如果存在就更新新的value。如果不存在,就插入一条记录。

  1. 首先根据key和hash0计算桶号以及tophash,然后在哈希桶中根据tophash查找,如果一个位置的tophash为"空"(tophash <= 1),说明该位置以及后面都为空,没有该key。因此直接将k-v存放在此处即可。
  2. 如果匹配到了相同的tophash,还要对比key是否相等。key值相等,就直接修改val。
  3. 如果key不相等,继续查找,直到找不到,如果找不到就找一个空位用来存放数据。
  4. 判断是否找到了空位,如果没有找到,需要创建一个溢出桶,将数据存放入溢出桶中。
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // 如果没有初始化就直接panic
    if h == nil {
        panic(plainError("assignment to entry in nil map"))
    }

    ...  // 省略了一些调试相关代码

    // 防止并发写入,并发写入就panic
    if h.flags&hashWriting != 0 { // 为1表示发生并发写
        throw("concurrent map writes")
    }
    // 计算哈希值
    hash := t.hasher(key, uintptr(h.hash0))

    // 设置正在写入的标志
    h.flags ^= hashWriting

    // buckets内存空间的滞后申请,如果使用makemap_small来创建map,就会在此时申请空间
    if h.buckets == nil {
        h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
    }

again:
    // 计算桶号
    bucket := hash & bucketMask(h.B)
    // 如果map正在扩容,需要额外做一些扩容的工作,后面再讲,暂时不关注
    if h.growing() {
        growWork(t, h, bucket)
    }
    // 获取key可能存在的哈希桶地址
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    // 计算tophash
    top := tophash(hash)

    var inserti *uint8
    var insertk unsafe.Pointer
    var elem unsafe.Pointer

    // 下面这个循环用来判断当前key是否存在,如果存在就直接修改数据,如果不存在就找一个可以存放数据的位置
bucketloop:
    for {
        // 首先要查询key是否已经存在
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                // 判断当前哈希桶是否是空的,如果是空的说明key不存在,找一个位置来存放k-v
                if isEmpty(b.tophash[i]) && inserti == nil {
                    // 记录插入的tophash、k、v的地址
                    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))
                }
                // 哈希桶是空的,说明不存在该key,直接调出最外层循环
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            // 匹配到了tophash,但是不一定是要找的key,因为不同的key,tophash可能相同
            // 计算key在哈希桶中的地址
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            // 不是要找的key,继续循环
            if !t.key.equal(key, k) {
                continue
            }
            // key已经存在,更新对应的值  
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            // val已经更新直接跳到done
            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)) {
        hashGrow(t, h)
        goto again // Growing the table invalidates everything, so try again
    }

    if inserti == nil {
        // 说明当前桶或者当前桶和溢出桶没有可用的槽位了,需要再分配一个溢出桶
        newb := h.newoverflow(t, b)
        // 然后获取tophash、k、v存放的地址
        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 { // 为0,表示存在并发写,别的线程给置为0
        throw("concurrent map writes")
    }
    h.flags &^= hashWriting // 关闭写入标识
    if t.indirectelem() {
        elem = *((*unsafe.Pointer)(elem))
    }
    return elem
}
  • map的删除

map的删除首先要查找key值是否存在,如果存在,就删除key-val,如果val中存在指针,就需要删除,因为需要解除对该指针的引用,以便垃圾回收器回收垃圾,否则就不用删除。

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	
    ...  // 省略了调试相关代码
    // 防止并发写入
    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }    

    // 计算哈希值 
    hash := t.hasher(key, uintptr(h.hash0))

    h.flags ^= hashWriting

    // 计算桶号
    bucket := hash & bucketMask(h.B)
    // 如果map正在扩容,需要进行扩容工作,稍后介绍
    if h.growing() {
        growWork(t, h, bucket)
    }
    // 根据桶号计算哈希桶地址
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    bOrig := b
    // 计算tophash
    top := tophash(hash)
    // 查找要删除的key是否存在
search:
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                // 如果tophash为0,说明key不存在,直接跳出循环
                if b.tophash[i] == emptyRest {
                    break search
                }
                continue
            }
            // 找到了相匹配的tophash,还需要对比key值是否相等
            // 计算key在数组中的地址
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            k2 := k
            if t.indirectkey() {
                k2 = *((*unsafe.Pointer)(k2))
            }
            // 对比key值,不相等就继续查找
            if !t.key.equal(key, k2) {
                continue
            }
            // 到此为止,说明已经找到了key-val
            // 删除key-val
            if t.indirectkey() {
                *(*unsafe.Pointer)(k) = nil
            } else if t.key.ptrdata != 0 {
                memclrHasPointers(k, t.key.size)
            }
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            if t.indirectelem() {
                *(*unsafe.Pointer)(e) = nil
            } else if t.elem.ptrdata != 0 {
                memclrHasPointers(e, t.elem.size)
            } else {
                memclrNoHeapPointers(e, t.elem.size)
            }
            // 将对应的tophash置为1
            b.tophash[i] = emptyOne

        ...

        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
        }
    }
}
  • map的清空
func mapclear(t *maptype, h *hmap) {
	
    ...

    if h == nil || h.count == 0 {
        return
    }


    // 重用hmap结构体
    // 重置其中的字段
    h.flags ^= hashWriting

    h.flags &^= sameSizeGrow
    h.oldbuckets = nil
    h.nevacuate = 0
    h.noverflow = 0
    h.count = 0

    h.hash0 = fastrand()

    // Keep the mapextra allocation but clear any extra information.
    if h.extra != nil {
        *h.extra = mapextra{}
    }

    // makeBucketArray clears the memory pointed to by h.buckets
    // and recovers any overflow buckets by generating them
    // as if h.buckets was newly alloced.
    _, nextOverflow := makeBucketArray(t, h.B, h.buckets)
    if nextOverflow != nil {
        // If overflow buckets are created then h.extra
        // will have been allocated during initial bucket creation.
        h.extra.nextOverflow = nextOverflow
    }

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

  • map的扩容
emptyRest = 0 // 表明该位置及其以后的位置都没有数据  
emptyOne = 1 // 表明该位置没有数据  
evacuatedX = 2 // key/elem是有效的,它已经在扩容过程中被迁移到了更大表的前半部分  
evacuatedY = 3 // key/elem是有效的,它已经在扩容过程中被迁移到了更大表的后半部分  
evacuatedEmpty = 4 // 该位置没有数据,且已被扩容  
minTopHash = 5 // 一个被正常填充的tophash的最小值

func evacuated(b *bmap) bool {  
    h := b.tophash[0]  
    return h > emptyOne && h < minTopHash  
}

image.png

参考:
Golang源码探究 —— map
Go底层探索(四):哈希表Map上篇

go的map并发安全吗?为什么不安全?如何解决?

不安全

官方的faq里有说明,考虑到有性能损失,map没有设计成原子操作,在并发读写时会有问题。

Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

const (  
    ...  
    hashWriting = 4 // a goroutine is writing to the map  
    ...  
)  

type hmap struct {  
    ...  
    flags uint8  
    ...  
}


// Like mapaccess, but allocates a slot for the key if it is not present in the map.  
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {  

    ...  

    if h.flags&hashWriting != 0 {  // 为1表示发生并发写
        throw("concurrent map writes")  
    }  
    hash := t.hasher(key, uintptr(h.hash0))  

    // Set hashWriting after calling t.hasher, since t.hasher may panic,  
    // in which case we have not actually done a write.  
    h.flags ^= hashWriting  // 置为1,标记当前正在写入

    ...  
done:  
    if h.flags&hashWriting == 0 {  // 为0表示被别的协程置为0,发生并发写
        throw("concurrent map writes")  
    }  
    h.flags &^= hashWriting  // 写入完成后将该位置为0

    ...  

}

// mapaccess1 returns a pointer to h[key]. Never returns nil, instead  
// it will return a reference to the zero object for the elem type if  
// the key is not in the map.  
// NOTE: The returned pointer may keep the whole map live, so don't  
// hold onto it for very long.  
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {  
    ...  
    if h.flags&hashWriting != 0 {  // 为1表示发生并发写
        throw("concurrent map read and map write")  
    }  
    ...  
}
  • 读写锁
package main  

import (  
    "fmt"  
    "sync"  
    "time"  
)  

func main() {  
    var c = struct {  
        sync.RWMutex  
        m map[string]int  
    }{m: make(map[string]int)}  

    go func() { // 开一个goroutine写map  
        for j := 0; j < 1000000; j++ {  
            c.Lock()  
            c.m[fmt.Sprintf("%d", j)] = j  
            c.Unlock()  
        }  
    }()  
    go func() { // 开一个goroutine读map  
        for j := 0; j < 1000000; j++ {  
            c.RLock()  
            fmt.Println(c.m[fmt.Sprintf("%d", j)])  
            c.RUnlock()  
        }  
    }()  
    time.Sleep(time.Second * 20)  
}
  • sync.map
package main  

import (  
    "fmt"  
    "sync"  
)  

var m sync.Map  

func main() {  

    // 写  
    m.Store("dablelv", "27")  
    m.Store("cat", "28")  

    // 读  
    v, ok := m.Load("dablelv")  
    fmt.Printf("Load: v, ok = %v, %v\n", v, ok)  

    // 删除  
    m.Delete("dablelv")  

    // 读或写  
    v, ok = m.LoadOrStore("dablelv", "18")  
    fmt.Printf("LoadOrStore: v, ok = %v, %v\n", v, ok)  

    // 遍历  
    f := func(key, value interface{}) bool {  
        fmt.Printf("Range: k, v = %v, %v\n", key, value)  
        return true  
    }  
    m.Range(f)  
}

参考:
源码解读 Golang 的 sync.Map 实现原理
Golang sync.Map 原理(两个map实现 读写分离、适用读多写少场景)