七叶笔记 » golang编程 » 详解golang的数据类型和底层实现三

详解golang的数据类型和底层实现三

map字典,今天来说说golang的第三个引用数据类型map。map底层引用的数据结构是一个hashtable,都要map类型的变量进行传递也是浅拷贝上层的指针,底层的hashtable仍然使用的是同一个,看一下源码src/runtime/map.go/hmap

 type hmap struct {
count     int  //元素个数 len()获取
flags     uint8
B         uint8  // 扩容常量
noverflow uint16 // 溢出 bucket 个数
hash0     uint32 // hash seed

buckets    unsafe.Pointer // bucket 数组指针
oldbuckets unsafe.Pointer // 扩容时旧的buckets 数组指针
nevacuate  uintptr        // 扩容搬迁进度

extra *mapextra //记录溢出相关
}  
 type mapextra struct {
overflow    *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}  
 type bmap struct {
tophash [bucketCnt]uint8
}  

hmap的结构中buckets指向bucket结构也就是hash之后存储键和值的存储单元,每个key会根据hash算法归到同一个bucket中,当一个bucket中的元素超过8个的时候,hmap会使用extra中的overflow来扩展存储key。tophash是个长度为8的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈希值的高位存储在该数组中,由 于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的方式将bucket连接起来。

hash扩容,hash冲突导致hashtable性能退化,当 负载因子 = 键数量/bucket数量,负载因子过大会进行rehash重新组织hashtable,不同的hashtable对负载因子的阈值不一样,redis大于1会触发rehash,go在6.5才会rehash,因为go有8个位置存储键值对。

rehash扩容,新申请一个bucket空间,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。 考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,采用逐步搬迁策略,即每次访 问map时都会触发一次搬迁,每次搬迁2个键值对,hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申请的bucket。新的键值对被插入新的 bucket中。后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来。当oldbuckets中的键 值对全部搬迁完毕后,删除oldbuckets。

还有一种方式就是重新整理bucket数组,不断的增删,而键值对正好集 中在一小部分的bucket,这样会造成overflow的bucket数量增多,但负载因子又不高,这种方式的rehash会把松散的键值对 重新排列一次,以使bucket的使用率更高,进而保证更快的存取。

查询map的流程就是根据key计算出hash值,取出hash的低位和hmap的B取模确定bucket的位置,取hash的高位在tophash数组中查找tophash[i]是否存储和key是否相等,不相等继续向下寻找,没找到继续向下overflow的bucket继续寻找,当处于扩容的map,要先从oldbuckets寻找,没有找到返回对应的0值

插入过程,也是根据key计算出hash值,低位取模去定bucket位置,查看key是已经存在bucket存在就更新不存在就插入

常用的demo

 func main() {
m := map[int]int{1: 1}
go func() {
i := 0
for i < 10000 {
m[i] = i
i++
}
}()
time.Sleep(2 * time.Second)
fmt.Println(m)
}  

开启一个goroutinne去写一个map没问题,开启两个呢?

 func main() {
m := map[int]int{1: 1}
go func() {
i := 0
for i < 10000 {
m[i] = i
i++
}
}()
go func() {
i := 0
for i < 10000 {
m[i] = i
i++
}
}()
time.Sleep(2 * time.Second)
fmt.Println(m)
}
//fatal error: concurrent map writes  

报错,根本原因就是:并发的去读写map结构的数据了,所以,map不是并发安全的unsafe;解决方案就是加锁syn.RWMutex加上读写锁就没问题了

 func main() {
m := map[int]int{1: 1}
var s sync.RWMutex
go func() {
i := 0
for i < 10000 {
s.Lock()
m[1] = 1
s.Unlock()
i++
}
}()

go func() {
i := 0
for i < 10000 {
s.Lock()
m[1] = 1
s.Unlock()
i++
}
}()

time.Sleep(2 * time.Second)
fmt.Println(m)
}
//map[1:1]  

同时sync包提供了并发安全的map的封装,sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。

 func main() {
m := sync.Map{}
m.Store(1, 1)
go func() {
i := 0
for i < 10000 {
m.Store(1, 1)
i++
}
}()

go func() {
i := 0
for i < 10000 {
m.Store(1, 1)
i++
}
}()

time.m(time.Second)
fmt.Println(m.Load(1))
}  

sync.Map也是封装了锁的机制,有两个map一个用于读的read map,一个是提供读写的dirty map;read map优先,读不到加锁穿透读dirty map,同时count++,当计数到达一定值,就将read map用dirty map进行覆盖,典型的空间换时间的方式读写分离。

相关文章