真希望你也明白runtime.Map和sync.Map

news2025/1/20 5:42:58

Map 官方介绍

One of the most useful data structures in computer science is the hash table. Many hash table implementations exist with varying properties, but in general they offer fast lookups, adds, and deletes. Go provides a built-in map type that implements a hash table.
哈希表是计算机中最有用的数据结构之一。提供快速查找、添加和删除。 Go 提供了一个实现哈希表的内置Map类型。

Hash冲突

那对于Hash的一个最重要的问题,就是hash冲突。下面我们看一下常用的解决方案。

开放寻址法

开放寻址法想象成一个停车问题。若当前车位已经有车,则继续往前开,直到找到一个空停车位。
go-32-001

上图,每个方格子,就是一个车位,当一辆车来的时候,会依次查询是否有空位,如果没有,则继续向后面找,如果发现空位置,就会停到空位置中。

go-32-002

下面看一下,我们的代码是如何实现的?

go-32-003

go-32-004

  1. m[“面向加薪学习”]=“从0到Go语言微服务架构师-训练营”
  2. 要对键-“面向加薪学习”,进行hash
  3. 拿到全体格子的总数,然后取模
  4. 如果取模发现是位子1,但是发现1已经被别人占了,那么就向后走,直到有空位,再把自己放进去。

看了上面的步骤是不是和停车,是一个道理?

那我们再看,如果想读取数据的时候:

  1. 同样对J键进行hash
  2. 拿到全体格子的总数,然后取模
  3. 找到位置是1,但是发现key不一样,它可能在后面,就一直向后查找。

拉链法

go-32-005

go-32-006

  1. m[“面向加薪学习”]=“从0到Go语言微服务架构师-训练营”
  2. 要对键-“面向加薪学习”,进行hash
  3. 找到对应到槽位,每个槽位并不存储具体数据,只是一个指针,它指向下面的链表
  4. 当新增数据的时候,会把数据添加到链表头部(上图中黄色小球为例)

Go语言的Map

runtime/map.go,看到hmap这个结构体,它就go语言的map

type hmap struct {
    count     int
    flags     uint8
    B         uint8 
    noverflow uint16
    hash0     uint32
    buckets    unsafe.Pointer 
    oldbuckets unsafe.Pointer 
    nevacuate  uintptr
    extra *mapextra
}
  1. count 键值对的数量
  2. B 是以2为底,桶个数的对数
  3. hash0 hash的种子
  4. oldbuckets 旧的hash桶
  5. buckets hash桶

下面看一个Go语言的哈希桶具体长什么样?用bmap结构体表示

bucketCntBits = 3
bucketCnt     = 1 << bucketCntBits

type bmap struct {
    tophash [bucketCnt]uint8
}

bucketCntBits 一个哈希桶可以存放最大的KV键值对的数量
bucketCnt Hash桶的数量(左移3位,也就是8。)

tophash [8]uint8

uint8是无符号的8为数字,也就是1个字节。 tophash 是存储桶中的每个键的哈希值的顶部字节(1个字节)。同样,k和v也是对应的8个。然后在编译的时候,将所有键和值再打包,这样就避免了在bmap中固定K和V的类型,最后还有一个overflow的指针,指向一个溢出桶。

go-32-007

新建Map

package main

import "fmt"

func main() {
    m := make(map[string]int, 16)
}

16代表预计要有16个Key,当然你也可以放更多的Key,Map会扩容,后面我们介绍到。

下面到命令行执行 go build -gcflags -S main.go

go-32-008

看到它调用了runtime.makemap这个函数。到runtime/map.go中,找到makemap()函数。

func makemap(t *maptype, hint int, h *hmap) *hmap {
    ...
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()
	
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B
}
...
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
    h.extra = new(mapextra)
    h.extra.nextOverflow = nextOverflow
}
...

4行 新建hmap
6行 获取hash种子
9-11行 计算B的个数,根据初始化的时候传入的数据。(make(map[string]int, 16) 就是这个数字16)
15行 生成多个hash桶
16-19行 生成溢出桶,并存放在.extra中,当一个正常的Bmap装满数据后,会去到NextOverflow中找到空闲的溢出桶,因为Bmap字段中,也有个overflow的指针。(也就是说,一开始先保持空闲捅的指针,每个bmap数据也不多,当哪个桶装满了,就是那个桶的overflow指针指向原来闲置的的溢出桶地址,然后nextOverflow再继续指向下一个空闲的溢出桶,也就是nextOverflow永远指向下一个空闲的溢出桶,等待着哪个捅满了需要新桶来装数据了,再通过那个装满数据桶的overflow指向这个桶,然后NextOverflow接着移动指针指向新空闲桶)

go-32-009

Map读取数据

1.计算在哪个桶里?

Hash(“锅包肉”+hash0),如果生成的二进制是 0110001101001011,如果我们的HMap中的B是3,那么末尾取3位 011, 换算成十进制就是3,就可以拿到buket 3,由于数组是从0开始,所以也就是4号桶。

2.获取TopHash

获取二进制前8位01100011,换算成16位是0x63

3.遍历TopHash

到数组中遍历,看看哪个位置的tophash是0x63

4.TopHash相同

继续查看key,如果相等,就返回元素,如果不相等,继续对比查找。

5.TopHash不同

如果4号桶的数组都遍历完了,没有0x63的tophash,如果有溢出桶,那就再去溢出桶中查找。如果都没有找到,那就是找不到key所对应的元素。

Map写数据

1.找到对应的桶(桶自身或溢出桶)

2.找到对应的key

3.修改数据的值

4.如果这个桶里没有对应的key,那么就直接插入一个

Map扩容都做了什么?

if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
    hashGrow(t, h)
    ...
}

可以看到有2个条件可以触发Map的扩容

  1. hmap不在增加并且溢出因子很多

    func overLoadFactor(count int, B uint8) bool {
         return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
    }
    

    bucketCnt是8,loadFactorNum是13,loadFactorDen是2

  2. 太多的溢出桶(这个会形成非常长的链表,导致严重的性能下降)

go-32-010

go-32-011

看一下代码

func hashGrow(t *maptype, h *hmap) {
    ...
    oldbuckets := h.buckets
    newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
    ...
    h.B += bigger
    h.flags = flags
    h.oldbuckets = oldbuckets
    h.buckets = newbuckets
    if h.extra != nil && h.extra.overflow != nil {
        ...
    }
}
  • 3行 把原来的桶给oldbuckets
  • 4行 h.B+bigger进行创建新桶和溢出桶
  • 6行 更新B的值
  • 7行 更新flags
  • 8行 把oldbuckets给h.oldbuckets
  • 9行 把newbuckets给h.buckets
  • 10行 溢出桶如果不为空,更新新桶的溢出桶

此时,新桶和老桶都存在,还没涉及到数据迁移的问题,下面我们看
Hash(“锅包肉”+hash0),如果生成的二进制是 0110001101001011,如果我们的HMap中的B是1,那么末尾取1位 1, 换算成十进制就是1,现在扩容,B是2,末尾取2位,就是11,换算十进制就是3,也就是说,未来数据会分配到buket-1和buket-3上。
接下来看如何处理数据

func growWork(t *maptype, h *hmap, bucket uintptr) {
    evacuate(t, h, bucket&h.oldbucketmask())

    // evacuate one more oldbucket to make progress on growing
    if h.growing() {
        evacuate(t, h, h.nevacuate)
    }
}

growWork()将旧桶上的数据放到新桶中去。

数据迁移完成后,把旧桶给GC了。

Map是并发安全的吗?

基于上面的学习,我们也可以看到 扩容前和扩容后, 当旧桶和新桶同时存在的时候,小明发起读数据,小刚发起写数据,小刚就会进入旧桶,进行数据迁移,那么小明很有可能在读取的时候,旧桶的数据已经被迁移到了新桶中,这样数据就会读错乱。

下面看一下Snyc.Map(注:上面的map是runtime包下的)

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[any]*entry
    misses int
}

type readOnly struct {
    m map[any]*entry
    amended bool 
}

type entry struct {
    p unsafe.Pointer // *interface{}
}
  • 2行 mu 是一个锁
  • 8-11 行 read 对应readOnly的结构体,readOnly中的m是一个任意类型键和任意类型值的map,entry是包含一个unsafe.Pointer的指针p的结构体。
  • 10行 amended是修正的意思。
  • 4行 dirty是一个任意类型键和任意类型值的map
  • 5行 misses 未击中

go-32-013

func (m *Map) Store(key, value any) {
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }

    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        e.storeLocked(&value)
    } else {
        if !read.amended {
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

上面就是存储数据的代码。

2-5 如果read中中可以找到这个Key,并且没有被标记被删除,就tryStore(),试图去更新值。

7-24 如果read中不存在这个Key或者被标记为已删除的情况,此时加锁/解锁。

9-13 再次读取read,此时已经找到了Key,如果entry被删除了,那么就把这个key和value存储到dirty的map中

14-16 dirty的map中存在这个key,更新这个值

17 到这一步,这个判断证明read和dirty都没有这个key,如果read的amended为假,证明read和dirty的两个map中的数据是相等的

18 如果dirty是nil,就把read的数据都放到dirty中,否则dirty有数据,就怎么都不做,直接返回。

19 标记amended为true,证明read和dirty不同了

21 把数据放到dirty的map中。

单协程代码演示

go-32-012

上图,从goland中打印出来的消息看,数据都在dirty的map中。

func (m *Map) Load(key any) (value any, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

2-3 到read中的map查找key

4-13 read中没找到,并且amended为真,意味read和dirty,这2个map数据不一致。(dirty的数据通常是多的)

5-12 加锁,操作dirty的map

6-7 再次读取map

8 read中仍然找不到这个key,并且amended为真

9 去dirty中读取该key

10 给misses +1,如果m.misses == len(m.dirty),那么就把m.dirty放到read中的m变量里,然后dirty设置nil,misses设置为0

14 read中没找到,但是amended为假,说明read和dirty数据相同,所以,直接返回nil,false

17 read中找到了entry,直接调用entry的load()方法就可以了

    var m sync.Map
    num := 100
    var w sync.WaitGroup
    w.Add(num)
    m.Store("《Go语言极简一本通》第4次印刷", 1)
    m.Store("《Go语言微服务架构核心22讲》", 2)
    m.Store("《Go语言+Redis》实战课", 3)
    m.Store("《Go语言+RabbitMQ》实战课", 4)
    m.Store("《从0到Go语言微服务架构师》训练营", 5)
    m.Store("《Web3与Go语言》实战课", 6)

    for i := 0; i < num; i++ {
        go func() {
            v2, _ := m.Load("《Web3与Go语言》实战课")
            fmt.Println(v2)
            w.Done()
        }()
    }
    w.Wait()
    fmt.Println(m)

go-32-014

删除方法源代码分析

func (m *Map) Delete(key any) {
    m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            delete(m.dirty, key)
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if ok {
        return e.delete()
    }
    return nil, false
}

6-7 读取read中的map的key

8 如果没找到,并且amended为真,证明read和dirty中的map数据不相等

9-17 加锁操作dirty的map

10-12 再次读取read中的map,如果没找到,并且amended为真

13 查找dirty的map中key

14 删除dirty的map中key

15 判断是否把dirty的map提升到read中去

19-20 在read中找到了entry,那么就直接调用entry的delete()

在main协程内,执行删除操作

    var m sync.Map
    m.Store("《Go语言极简一本通》第4次印刷", 1)
    m.Store("《Go语言微服务架构核心22讲》", 2)
    m.Store("《Go语言+Redis》实战课", 3)
    m.Store("《Go语言+RabbitMQ》实战课", 4)
    m.Store("《从0到Go语言微服务架构师》训练营", 5)
    m.Store("《Web3与Go语言》实战课", 6)
    m.Delete("《Web3与Go语言》实战课")
    fmt.Println(m)

go-32-015

多协程删除操作

    var m sync.Map
    num := 100
    var w sync.WaitGroup
    w.Add(num)
    m.Store("《Go语言极简一本通》第4次印刷", 1)
    m.Store("《Go语言微服务架构核心22讲》", 2)
    m.Store("《Go语言+Redis》实战课", 3)
    m.Store("《Go语言+RabbitMQ》实战课", 4)
    m.Store("《从0到Go语言微服务架构师》训练营", 5)
    m.Store("《Web3与Go语言》实战课", 6)

    for i := 0; i < num; i++ {
        go func() {
            m.Delete("《Web3与Go语言》实战课")
            w.Done()
        }()
    }
    w.Wait()
    fmt.Println(m)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/84645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Grafana监控大屏配置参数介绍(二)

Grafana 系列文章&#xff0c;版本&#xff1a;OOS v9.3.1 Grafana 的介绍和安装Grafana监控大屏配置参数介绍&#xff08;一&#xff09;Grafana监控大屏配置参数介绍&#xff08;二&#xff09; 上一篇文章已经介绍了图表可视化配置部分的 Panel options、Tooltip、Legend 3类…

音视频行业大势如何,优势在哪?

电信行业的变革&#xff1a; 从1G语音、2G短信、3G图片语音、4G视频到5G未来可期的新时代&#xff0c;见证了音视频行业的磅礴发展。 技术更新慢且门槛高 技术更新慢&#xff0c;技术门槛高&#xff0c;大部分技术沿用至今&#xff0c;依然保持生命力&#xff0c;技术人员成型…

http协议和websocket协议

http协议 HTTP 即超文本传输协议&#xff0c;是一种获取网络资源 (例如图像、HTML 文档) 的应用层协议&#xff0c;它是互联网数据通信的基础&#xff0c;由请求和响应构成。通常&#xff0c;首先客户端会发送 HTTP 请求(在请求报文中会指定资源的 URL)&#xff0c;然后用传输…

DocArray 0.20.0 发布!新增 Milvus 后端支持,更好地嵌套数据搜索,新增 RGB-D 格式的 3D 模型表示...

DocArray 是一个用于处理、传输和存储多模态数据的 Python 工具包。DocArray 提供便捷的多模态数据处理功能&#xff0c;具备基于 Protobuf 提供高性能的网络传输性能&#xff0c;同时也为多种向量存储方案提供统一的 API 接口。GitHub&#xff1a;github.com/docarray/docarra…

AU如何为你的人声增加空旷感?

你知道怎么使用AU给你的声音添加延迟效果&#xff0c;让你的声音具有空旷感和弱回声的效果。在这里我们可以使用插件达到这个目的。 在使用模拟延迟插件之前呢&#xff0c;我们可以去创建一个立体声总音轨&#xff0c;创建方式如图&#xff0c;跟着序号走&#xff0c;我们就可以…

CSS -- 06. CSS高阶技巧总结

文章目录CSS高阶技巧1 精灵图(sprites)1.1 为什么使用精灵图1.2 精灵图的使用2 字体图标2.1 字体图标的产生2.2 字体图标的优点2.3 字体图标的下载2.4 字体图标的引入2.5 字体图标的追加3 CSS三角形4 CSS用户界面样式4.1 鼠标样式 cursor4.2 表单的轮廓线4.3 防止拖拽文本域 re…

JAVA毕业设计——基于Springboot+vue的心理咨询管理系统(源代码+数据库)

github代码地址 https://github.com/ynwynw/psychlolgyhealth-public 毕业设计所有选题地址 https://github.com/ynwynw/allProject 基于Springbootvue的心理咨询管理系统(源代码数据库) 一、系统介绍 本项目分为管理员与普通用户两种角色 管理员角色包含以下功能&#xff…

[附源码]Python计算机毕业设计SSM基于Web美食网站设计(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Kotlin标准库函数

Kotlin标准库中包含了几个函数&#xff0c;它们的目的就是可以在对象的上下文中执行代码块。当我们调用该Lambda表达式时&#xff0c;它会形成一个临时的作用域。在该范围内&#xff0c;可以访问不带名称的对象&#xff0c;此类函数称为作用域函数。包括: apply函数let函数run…

Java开发才不到3年,来面试开口要25K,面完连10K都不想给

前言 我的好朋友兼大学同学老左家庭经济情况不错&#xff0c;毕业之后没两年自己存了点钱加上家里的支持&#xff0c;自己在杭州开了一家网络公司。由于公司不是很大所以公司大部分的开发人员都是自己面试的&#xff0c;近期公司发展的不错&#xff0c;打算扩招也面试了不少人。…

[l论文解析]Classifier-Free Diffusion Guidance

paper link&#xff1a;https://openreview.net/pdf?idqw8AKxfYbI 文章目录OverviewWhat problem is addressed in the paper?What is the key to the solution?What is the main contribution?Potential fundamental flaws; how this work can be improved?Content关于 c…

Java 字符串 split 的一个反直觉陷阱

最近生产环境遇到一个奇怪的数组下标越界报错&#xff0c;如下图代码所示&#xff0c;我们可以肯定的是 fieldName 变量不为空&#xff08;不是空字符串&#xff0c;也不是 null&#xff09;&#xff0c;但是代码执行到读取 names[0] 变量的时候&#xff0c;抛出了一个 数组下标…

5G无线技术基础自学系列 | 抗衰落技术

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 无线信道是随机时变信道&#xff0c;信…

【云计算与大数据技术】文件存储格式行式、列式、GFS、HDFS的讲解(图文解释 超详细)

一、分布式文件系统 文件系统最后都需要以一定的格式存储数据文件&#xff0c;常见的文件存储布局有行式存储、列式存储以及混合式存储三种&#xff0c;不同的类别各有其优缺点和适用的场景&#xff0c;在目前的大数据分析系统中&#xff0c;列式存储和混合式存储方案因其特殊…

mysql 数据库设计三大范式

1. 什么是设计范式 设计表的依据&#xff0c;按照范式设计出来的表&#xff0c;不会出现数据的冗余 数据库的设计范式是数据库设计所需要满足的规范&#xff0c;满足这些规范的数据库是简洁的、结构清晰的&#xff1b;反之则是乱七八糟&#xff0c;不仅会给开发人员制造麻烦&a…

大数据面试之Hive常见题目

大数据面试之Hive常见题目 1. Hive的架构 1、重点四个器&#xff1a;解释器&#xff08;SQL Parser&#xff09;、Driver&#xff1a;编译器&#xff08;Compiler&#xff09;&#xff0c;优化器&#xff08;Optimizer&#xff09;&#xff0c;执行器&#xff08;Executor&…

基于Python+Django的在线学习交流平台

在各学校的教学过程中&#xff0c;直播授课管理是一项非常重要的事情。随着计算机多媒体技术的发展和网络的普及&#xff0c;“基于网络的学习模式”正悄无声息的改变着传统的直播学习模式&#xff0c;“基于网络的直播教学平台”的研究和设计也成为教育技术领域的热点课题。采…

BEPUphysicsint定点数3D物理引擎介绍

帧同步的游戏中如果用物理引擎&#xff0c;为了保证不同设备上的结果一致,需要采用定点数来计算迭代游戏过程中的物理运算。也就是我们通常说的定点数物理引擎(确定性物理引擎)。本系列教程给大家详细的讲解如何在你的项目中内置一个确定性物理引擎。确定性物理引擎我们使用git…

es入门(中)

目录 6.Java api 实现es中的文档管理&#xff08;增删改&#xff09; 6.1 java 客户端简单获取数据 6.2结合spring-boot测试文档查询 配置环境 配置类 测试代码结构 简单的查询 对查询的请求设置参数 异步查询 6.4 结合spring-boot测试文档新增 6.5结合spring-boot…

C语言的预处理器无法先展开宏再拼接符号?可以!

背景 最近接到一个需求&#xff0c;要实现一个脚本&#xff0c;能提取.h文件里定义的所有全局变量的值&#xff0c;这些全局变量都是结构体变量&#xff0c;名字是结构体类型名加场景后缀——每个.h对应的场景都是唯一的&#xff0c;所以.h内所有变量名的后缀一致。 我的解决…