Go中的数据结构(二) | 青训营笔记

75 阅读2分钟

Map

  • 开放寻址法
  • 拉链法 Pasted image 20230510221809.png Go语言的map使用的是拉链法, 在runtime.hmap中, hmap被定义为
type hmap struct {
	count int
	flags uint8	
	B uint8
	nooverflow uint16
	hash0 uint32
	

	buckets unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate uintptr
	extra *mapextra
}

Pasted image 20230510222352.png 2^B个bucket, b map is a bucket for a Go map

type bmap struct {
// bucketCntBits = 3
// bucketCnt = 1 << bucketCntBits
  tophash [bucketCnt]uint8
}

map的初始化

  • make m := make(map[string]int, 10) Pasted image 20230510223101.png
  • 字面量 元素少于25个时, 转化为简单赋值
hash := map[string]int {
  "1": 2,
  "3": 4,
  "5", 6,
}
// 转化为
hash := make(map[string]int, 3)
hash["1"] = 2
hash["3"] = 4
hash["5"] = 6

元素多于25个时, 转化为循环赋值

hash := map[string]int {
	"1" : 1
	"2" : 2 
	...
	"26": 26
}
// 转化为
hash := make(map[string]int, 26)
vstatk := []string{"1", "2", ... , "26"}
vstatv := []int{1, 2, ..., 26}
for i := 0; i < len(vstak); i++ {
	hash[vstatk[i]] = vstatk[i]
}

map的访问

  1. 计算桶号 Pasted image 20230510224041.png
  2. 计算tophash

Pasted image 20230510223911.png 3. 匹配

Pasted image 20230510224212.png 从第二个桶中找到tophash等于0x5c的kv, 看k是不是我们想要的, 如果是则返回v. 如果碰撞, 继续线性查找, 如果该bucket也没有, 则去查找溢出桶, 如果都没有则该k不存在.

写入也类似

map扩容

当hash碰撞过多的时候, 溢出桶数量增加, 会退化成一个链表

Pasted image 20230510224803.png map溢出桶太多会导致严重的性能下降 runtime.mapassign()可能会触发扩容的情况

  1. 装载因子超过6.5(品骏每个槽6.5个key)
  2. 使用了太多溢出桶(溢出桶超过了普通桶)

map的扩容类型

  • 等量扩容 数据不多但是溢出桶太多了(整理)
  • 翻倍扩容 数据太多了

扩容步骤

  1. 创建一组新桶
  2. oldbuckets指向原有的桶数组
  3. buckets指向新的桶数组
  4. map标记为扩容状态
  5. 将所有的数据从旧桶驱逐到新桶
  6. 采用渐进式驱逐
  7. 每次操作一个旧桶时, 将旧桶数据驱逐到新桶
  8. 读取时不进行驱逐, 只判断读取新桶还是旧桶
  9. 所有的旧桶驱逐完成后
  10. oldbuckets回收

map的并发

func main() {
	m := make(map[int]int)
	go func() {
		for {
			_ = m[1]
		}
	}()
	go func() {
		for {
			m[2] = 2
		}
	}()
	select {}
}

无法编译fatal error: concurrent map read and map write A协程在桶中读数据时, B协程驱逐了这个桶, 那么A协程就会读到错误的数据或者找不到数据

Pasted image 20230511203607.png 解决方案

  • 给map加锁(mutex)
  • 使用sync.Map

Pasted image 20230511203912.png 追加"d":D

Pasted image 20230511204247.png 追加后的读写(misses ++ ), 先去read没有找到d, amended为true, 去dirty查找, 找到对应kv, misses ++

Pasted image 20230511204503.png dirty提升, misses = len(dirty)时, dirty提升为read的m, 置回初始状态

Pasted image 20230511210918.png 删除操作

  • 正常删除

Pasted image 20230511211202.png k置成nil后, GC会自动将v回收

  • 追加后删除

Pasted image 20230511211420.png 后面需要提升的话, 要特殊处理

Pasted image 20230511211556.png 提升后被删key还需要特殊处理 不是读写分离, 而是普通读写和追加分离

总结

  • Go语言使用拉链实现了hashmap
  • 每一个桶中存储键哈希的前8位
  • 桶超出8个数据, 就会存储到溢出桶中
  • 装载系数或者溢出桶的增加, 会触发map扩容
  • 扩容可能并不是增加桶数, 而是整理
  • map扩容采用渐进式, 桶被操作时才会重新分配
  • map才扩容的时候会有并发问题
  • sync.Map使用了两个Map, 分离了扩容问题
  • 不会引发扩容的操作(查 改)使用read map
  • 会引发扩容的操作(新增)使用dirty map
  • 读多写多, 追加少的时候性能好