Go 面试 Go Map 的并发安全问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
YunFun
V2EX    程序员

Go 面试 Go Map 的并发安全问题

  •  
  •   YunFun 323 天前 2915 次点击
    这是一个创建于 323 天前的主题,其中的信息可能已经有所发展或是发生改变。

    面试官:你好,我们来聊下 Go 语言的 map 。首先,请聊下 Go 语言的 map 是不是并发安全的?

    应试者:不是的。Go 语言的 map 不是并发安全的。如果在多个 goroutine 同时读写同一个 map 时,会出现竞态条件( race condition ),可能导致程序运行出错,甚至崩溃。

    为了证明这一点,我可以展示一个并发不安全的示例:

    package main import ( "fmt" "sync" ) func main() { m := make(map[int]int) var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(n int) { defer wg.Done() m[n] = n }(i) } wg.Wait() fmt.Println("Map 的大小:", len(m)) } 

    这段代码可能会出现以下问题:

    1. 并发写入可能导致 map 数据损坏
    2. 可能会触发运行时 panic
    3. 最终的 map 大小可能不是预期的 100

    面试官:OK 。那当我们需要在并发场景下使用 map 时,你有什么好的解决方案吗?

    应试者:在 Go 语言中,主要有三种解决方案:

    1. 使用 sync.Mutex 互斥锁来保护 map
    type SafeMap struct { sync.Mutex m map[int]int } func (sm *SafeMap) Set(key, value int) { sm.Lock() defer sm.Unlock() sm.m[key] = value } func (sm *SafeMap) Get(key int) (int, bool) { sm.Lock() defer sm.Unlock() val, exists := sm.m[key] return val, exists } 
    1. 使用 sync.RWMutex,允许并发读,但写入互斥
    type SafeMap struct { sync.RWMutex m map[int]int } func (sm *SafeMap) Get(key int) (int, bool) { sm.RLock() defer sm.RUnlock() val, exists := sm.m[key] return val, exists } 
    1. 使用 sync.Map,这是 Go 标准库提供的并发安全的 map 实现
    var m sync.Map // 存储 m.Store("key", "value") // 读取 value, ok := m.Load("key") // 删除 m.Delete("key") 

    面试官:能详细解释一下为什么普通的 map 不是并发安全的吗?这背后的机制是什么?

    应试者:这涉及到 Go 语言 map 的底层实现。在 Go 的源码中( runtime/map.go ),map 的结构大致是这样的:

    type hmap struct { count int // 元素个数 flags uint8 // 状态标志 B uint8 // 桶的对数 noverflow uint16 // 溢出桶的近似数 hash0 uint32 // 哈希种子 buckets unsafe.Pointer // 桶数组 oldbuckets unsafe.Pointer // 旧桶数组,在扩容时使用 // ... 其他字段 } 

    并发不安全的根本原因在于:

    1. map 的内部操作(如插入、删除)不是原子的
    2. 扩容过程中会修改桶的结构
    3. 多个 goroutine 同时操作会导致数据竞争

    具体来说,一个简单的写入操作可能包含多个步骤:

    • 计算 key 的哈希值
    • 定位到具体的桶
    • 在桶中找到空位
    • 写入数据

    这些步骤如果被并发执行,就会导致不可预期的结果。

    面试官sync.Map 是如何解决这些并发问题的?能详细介绍一下它的实现原理吗?

    应试者sync.Map 的核心设计是读写分离和优化的并发控制。我们可以看一下它的大致结构:

    type Map struct { mu Mutex read atomic.Value // readOnly dirty map[interface{}]*entry misses int } type readOnly struct { m map[interface{}]*entry amended bool // 是否有新的数据在 dirty 中 } 

    它的主要优化策略包括:

    1. 双层存储:

      • read map:无锁读取
      • dirty map:需要加锁的可写 map
    2. 读优化:

      • 优先从 read map 读取
      • 使用原子操作 atomic.Value 保证读取的线程安全
    3. 写入机制:

      • 先尝试在 read map 中更新
      • 如果不成功,则加锁操作 dirty map
    4. 动态提升:

      • dirty map 被频繁访问时,会将其提升为 read map

    实际的读写流程大致如下:

    func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 首先无锁读取 read map read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { // 如果 read map 没有,且有新数据,则加锁查询 dirty map m.mu.Lock() // 双检查,避免重复加锁 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // 记录未命中次数,可能会晋升 dirty map m.missLocked() } m.mu.Unlock() } // ... 返回结果 } 

    面试官:那么,sync.Map 是不是在所有并发场景下都是最佳选择?

    应试者:不是的。sync.Map 有其特定的适用场景和局限性:

    适用场景:

    1. 读操作明显多于写操作
    2. key 是动态增长的
    3. 元素命中率较高

    不适用场景:

    1. 写操作频繁
    2. key 是有限且提前确定的
    3. 需要有序遍历 map
    4. 需要对 key 进行排序或自定义比较的场景

    性能建议:

    • 对于写多读少的场景,传统的 sync.Mutex + map 可能更高效
    • 对于读写均衡的场景,可以考虑分片锁等自定义方案

    面试官:最后,你能分享一个实际工作中处理并发 map 的最佳实践吗?

    应试者:在高并发缓存场景,我曾使用分片锁方案来优化 map 的并发性能:

    type ShardedMap struct { shards []map[string]interface{} locks []sync.RWMutex shardCount int } func NewShardedMap(shardCount int) *ShardedMap { sm := &ShardedMap{ shards: make([]map[string]interface{}, shardCount), locks: make([]sync.RWMutex, shardCount), shardCount: shardCount, } for i := 0; i < shardCount; i++ { sm.shards[i] = make(map[string]interface{}) } return sm } func (sm *ShardedMap) getShard(key string) (map[string]interface{}, *sync.RWMutex) { hash := fnv32(key) shardIndex := hash % uint32(sm.shardCount) return sm.shards[shardIndex], &sm.locks[shardIndex] } func (sm *ShardedMap) Set(key string, value interface{}) { shard, lock := sm.getShard(key) lock.Lock() defer lock.Unlock() shard[key] = value } func fnv32(key string) uint32 { hash := uint32(2166136261) for i := 0; i < len(key); i++ { hash *= 16777619 hash ^= uint32(key[i]) } return hash } 

    这种方案的优点:

    1. 降低锁的粒度
    2. 提高并发性能
    3. 可以根据业务动态调整分片数

    面试官:很好,你对 map 的并发问题理解的相当充分。

    应试者:非常感谢!


    更多 Go 高质量内容: https://portal.yunosphere.com

    欢迎关注我,经常分享有用的 Go 知识 / 面试 / 创业经历 / 其他编程知识

    • 公众号:GopherYes
    • B 站:YunFuns
    • 知乎、掘金、头条号:YunFun
    • 小红书号:986261983
    38 条回复    2024-11-29 18:13:58 +08:00
    wolfsun
        1
    wolfsun  
       323 天前   1
    很佩服楼主能有这样的行动力做一件事,在这个时代就是要这样才能搞到钱,也是挺难的。
    tcpdump
        2
    tcpdump  
       323 天前
    笑死,除了面试,很少会专研这个,你写业务的时候,写测试,不就测出来了吗?面试钻某个点的都是装的
    yingxiangyu
        3
    yingxiangyu  
       323 天前
    @tcpdump #2 但是实际上问的这些问题本质都是对哈希表的优化,就算不是面试,有多线程大量读写共享最终也是这个方案,跟语言、面试也没啥关系
    tcpdump
        4
    tcpdump  
       323 天前
    @yingxiangyu 对啊,就是内存共享、争用、锁、一致性啊,业务都要考虑的,不要关注代码层,应该是先有思路再去专研,实现业务需求
    DefoliationM
        5
    DefoliationM  
       323 天前 via Android
    好的,知道了,回去等通知吧。
    YunFun
        6
    YunFun  
    OP
       323 天前
    @tcpdump 是面试啊
    YunFun
        7
    YunFun  
    OP
       323 天前
    @yingxiangyu 对的哈哈
    YunFun
        8
    YunFun  
    OP
       323 天前
    @DefoliationM 经典等通知
    YunFun
        9
     
    OP
       323 天前
    @wolfsun 感谢感谢,喜欢研究技术,也做点自己喜欢的事,能有收入就更好了
    grzhan
        10
    grzhan  
       323 天前
    fastcache 也是分片锁的思路,切成 512 个 buckets ,进一步最主要就是针对 GC 做了优化,索引用 map[int]int noscan 来减小 GC 扫描开销,实际 key,value 放在一个自己手动 mmap 分配管理的 chunks ([][]bytes )里,跳过了 golang 自己的堆 gc 。这套思路上生产很多场景应该是够用了
    CLMan
        11
    CLMan  
       323 天前
    或许没有一个编程语言能逃过八股文。
    kingcanfish
        12
    kingcanfish  
       323 天前
    什么时候卖课
    cydian
        13
    cydian  
       323 天前
    @YunFun 希望你官网的课程可以开放几节公开课,希望能看到你课程讲解的深度 和 你的表达能力。
    CEBBCAT
        14
    CEBBCAT  
       323 天前
    感觉写得还不错,前两天另外一个人发 goroutine 还是什么的,被我评论区“省流”了。

    虽然写得不错,但我读完感觉更期望有若干篇从软件设计角度研讨的文章楼上也有人提到,这些代码背后其实就是运用不同策略应对不同场景的需求那样的话想必能够提纲挈领,促使读者举一反三
    bitfly
        15
    bitfly  
       323 天前 via Android
    这个我用过
    在读取一段 ip 列表时 比如有 100 万个 ip 需要验证是否能 ping 通
    但是不能顺序读取
    需要随机从里面的列表读取
    且不能漏 且需要并发每次读取 100 个不然一个个读取要几个月时间
    且有超时条件
    就需要用这个方法
    但是也偶尔会触发 panic
    如果不 panic 那处理的效率是 c#的数倍
    moudy
        16
    moudy  
       323 天前
    @tcpdump #2 测出来算你烧高香。实际很多犄角旮旯的用错了测不出来,上线才会暴雷
    COW
        17
    COW  
       323 天前 via Android
    八股文太多了,如果 op 能结合实际场景去分享,我觉得更吸引人一点。
    ryan961
        18
    ryan961  
       323 天前
    面试官:不错,基础很扎实。能再谈一谈你对 Go 1.24 即将落地的 swiss table 的看法吗?
    6HWcp545hm0RHi6G
        19
    6HWcp545hm0RHi6G  
       323 天前
    @ryalu 哈哈哈哈 接着问
    ranye777
        20
    ranye777  
       323 天前
    @moudy #16 是的,线上出个并发的偶现问题是会搞死人的
    Felldeadbird
        21
    Felldeadbird  
       323 天前
    感谢楼主分享。我写 go 才小半年,还是菜鸟一个,问我 map 的我都答不上。
    gongxuanzhang
        22
    gongxuanzhang  
       323 天前
    在 Java 八股面前,这玩意和小儿科一样
    zbatman
        23
    zbatman  
       323 天前
    面试官:好,那喜欢爸爸还是妈妈?
    YunFun
        24
    YunFun  
    OP
       323 天前
    @grzhan 对,主要就是 GC 优化,自定义内存管理减少 Go 自己的 GC 。。。内存也更可控了。
    table cellpadding="0" cellspacing="0" border="0" width="100%"> YunFun
        25
    YunFun  
    OP
       323 天前
    @CLMan 哈哈哈哈哈,八股也算一种真实需求了
    YunFun
        26
    YunFun  
    OP
       323 天前
    @kingcanfish 在卖了在卖了
    YunFun
        27
    YunFun  
    OP
       323 天前
    @cydian 听劝。改版下官网,最近出个公开课模块哈哈,其实前段时间搞了点免费课程 https://yunosphere.com ,最近也在一边更新一边重新规划公开的内容~ 欢迎关注 ,感谢宝贵的建议
    YunFun
        28
    YunFun  
    OP
       323 天前
    @CEBBCAT 好的,后续会考虑搞一些业务实例来配合解读这样的,坚持研究跟更新
    YunFun
        29
    YunFun  
    OP
       323 天前
    @bitfly ,这是大佬
    YunFun
        30
    YunFun  
    OP
       323 天前
    @COW 后边会慢慢尝试从实例或者合理的场景出发聊一些内容,多谢建议 欢迎关注~ 我会尝试坚持产出嘿嘿
    YunFun
        31
    YunFun  
    OP
       323 天前
    @ryalu 哈哈 有机会写写看~
    YunFun
        32
    YunFun  
    OP
       323 天前
    @ranye777 对,,真的到了生产上其实还是很多奇葩问题的,八股大多数时候只能搞定面试
    YunFun
        33
    YunFun  
    OP
       323 天前
    @gongxuanzhang #22 哈哈哈,jvm 原理都是入门对吧
    YunFun
        34
    YunFun  
    OP
       323 天前
    @Felldeadbird #21 能帮到你最好哈哈,互相学习 欢迎关注
    YunFun
        35
    YunFun  
    OP
       323 天前
    @zbatman #23 你是懂面试的
    gongxuanzhang
        36
    gongxuanzhang  
       323 天前
    @YunFun 这点锁的知识连培训班都得教了
    YunFun
        37
    YunFun  
    OP
       323 天前
    @gongxuanzhang #36
    哥们,本来我是做好被喷卖课的准备的,结果倒是被喷不如培训班
    首先我是卖课需要积累自己的内容所以在摸索着做,想解决的是想学学其他技能这部分人的需求顺便看能否赚点钱,不做培训班,也没那能力偶
    其次培训班面向的是包就业,这不是我的目的。真的工作经验积累也不是培训班那点套路东西能搞定的不是
    最后培训班也是要收费的,我花时间写了,至少这里还是免费的
    gongxuanzhang
        38
    gongxuanzhang  
       322 天前
    @YunFun 首先对伤害到你感到抱歉,并没有想嘲讽你和针对你。
    我想表达的就是关于 Map 的线程安全问题和如何解决衍生的一系列知识。
    我觉得对于 Java 面试来说这个就是非常初级的八股,哪怕和 JVM 相关也是初级八股。
    这个初级不是说知识有多简单,而是因为它被问的频繁,我不太了解 go ,仅仅是在语法层面而已。
    祝你大卖
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2743 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 68ms UTC 09:09 PVG 17:09 LAX 02:09 JFK 05:09
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86