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: 是一个指针,指向溢出桶,当该桶不够用时,就会使用溢出桶
}
- 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值是否相等,如果相等就说明找到了。
// 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。如果不存在,就插入一条记录。
- 首先根据key和hash0计算桶号以及tophash,然后在哈希桶中根据tophash查找,如果一个位置的tophash为"空"(tophash <= 1),说明该位置以及后面都为空,没有该key。因此直接将k-v存放在此处即可。
- 如果匹配到了相同的tophash,还要对比key是否相等。key值相等,就直接修改val。
- 如果key不相等,继续查找,直到找不到,如果找不到就找一个空位用来存放数据。
- 判断是否找到了空位,如果没有找到,需要创建一个溢出桶,将数据存放入溢出桶中。
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
}
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实现 读写分离、适用读多写少场景)