Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

35 | 并发安全字典sync.Map (下)

存储键值对,键类型为interface{}
不会增减键,允许变更值
参数类型检查
参数类型检查
包含keyType和valueType字段,类型为reflect.Type
包含sync.Map类型的字段m
新增键值对操作对性能影响最大
读操作多、写操作少时性能更好
脏字典转为只读字典,重建脏字典
否则,在锁的保护下存储到脏字典中
若只读字典中已存在键且未被标记为已删除,则直接存储,无需锁定
若只读字典没有,但脏字典可能有,则在锁的保护下访问脏字典
先在只读字典中查找,无需锁定
脏字典
只读字典
总结:方案二弥补了方案一的缺陷,但可能影响程序性能
Store方法
Load方法
ConcurrentMap类型的设计
使用反射类型进行键和值的类型检查
缺点:缺少灵活性,扩展性差
优点:方便
其他保证并发安全字典中的键和值的类型正确性的方案
性能影响
只读字典与脏字典互相转换
存储键值对
查找键值对
使用两个原生map作为存储介质
sync.Map内部使用原子操作存取键和值
方案二:动态设置类型,通过反射操作进行检查
方案一:完全确定键和值的类型,利用编译器进行检查
思考题
并发安全字典尽量避免使用锁
保证并发安全字典中的键和值的类型正确性
并发安全字典sync.Map

该思维导图由 AI 生成,仅供参考

你好,我是郝林,今天我们继续来分享并发安全字典 sync.Map 的内容。
我们在上一篇文章中谈到了,由于并发安全字典提供的方法涉及的键和值的类型都是interface{},所以我们在调用这些方法的时候,往往还需要对键和值的实际类型进行检查。
这里大致有两个方案。我们上一篇文章中提到了第一种方案,在编码时就完全确定键和值的类型,然后利用 Go 语言的编译器帮我们做检查。
这样做很方便,不是吗?不过,虽然方便,但是却让这样的字典类型缺少了一些灵活性。
如果我们还需要一个键类型为uint32并发安全字典的话,那就不得不再如法炮制地写一遍代码了。因此,在需求多样化之后,工作量反而更大,甚至会产生很多雷同的代码。

知识扩展

问题 1:怎样保证并发安全字典中的键和值的类型正确性?(方案二)

那么,如果我们既想保持sync.Map类型原有的灵活性,又想约束键和值的类型,那么应该怎样做呢?这就涉及了第二个方案。
在第二种方案中,我们封装的结构体类型的所有方法,都可以与sync.Map类型的方法完全一致(包括方法名称和方法签名)。
不过,在这些方法中,我们就需要添加一些做类型检查的代码了。另外,这样并发安全字典的键类型和值类型,必须在初始化的时候就完全确定。并且,这种情况下,我们必须先要保证键的类型是可比较的。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

`sync.Map`是Go语言中的一种并发安全字典类型,通过内部的原生字典和巧妙的设计,尽可能避免使用锁,提高了并发安全字典的性能。本文讨论了如何保证并发安全字典中的键和值的类型正确性,提出了两种方案:一种是在编码时确定键和值的类型,另一种是在程序运行时通过反射操作进行类型检查。这两种方案各有利弊,需要根据实际情况进行选择。此外,文章还提出了思考题,鼓励读者思考其他可能的方案。通过对`sync.Map`内部结构的分析,读者可以了解并发安全字典的适用场景和不同操作对性能的影响程度。整体而言,本文深入浅出地介绍了并发安全字典的设计原理和性能优化,为读者提供了有价值的技术参考。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(28)

  • 最新
  • 精选
  • sky
    郝大 go方面能推荐下比较成熟的微服务框架吗

    作者回复: 在我发布的Github优秀Go语言项目的思维导图里有。

    2018-11-01
    2
    12
  • 下雨天
    老师好,关于:sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。这句话,只读map里面的值可以被替换的话,为什么不需要加锁?不会 有读写冲突吗?

    作者回复: 【第一个层面】 因为这里面的只读字典 read 是通过 sync/atomic.Value 存储的,又正因为 read 是只读的,不存在增删键值对的情况,所以从 read 整体的层面可以安全地操作其中的某个键值对。 【第一个层面:更具体的细节】 在这种保护下,只要这里的键值对在数量上没有增减,就不会出现新数据丢失或者弄脏数据的情况。反例的话,可以看我最近写的这篇文章:https://mp.weixin.qq.com/s/ru161EtyQMrQVtWji0CqzQ 【第二个层面】 由于其中的每一个键值对(entry 结构)都可以保证自身的并发性( entry 内部只会存指针,因此用原子操作就可以保证并发读写的安全性),所以从 read 中单个键值对的层面也就有了并发安全性方面的保证。 【结论】 综合以上两种措施,这里才无需使用锁来保护。

    2020-10-20
    7
  • 疯琴
    这个设计很巧妙,在自己的开发中可以借鉴这种思想。有个问题请问老师: “脏字典中的键值对集合总是完全的”,而“read 和 dirty 互换之后 dirty 会置空”,那么重建的意思是不是这样的:在下一次访问 read 的时候,将 read 中的键值对全部复制到 dirty 中?

    作者回复: read 和 dirty 互换是分两步走的。Load 的时候如果发现“不得不去 dirty 中查找”的情况已经有很多了,就会把 dirty 作为新的 read,然后把 dirty 置为 nil。之后,在 Store 的时候,如果发现健是新的,而且是对于新 read 的第一个新健(此时 dirty 必定为 nil),那么就重新初始化 dirty,然后把新 read 中的有效键一个一个地存入 dirty。 另外你可以尝试阅读一下 sync.Map 的源码,写得还是挺清晰的。可以配合着这里的文章去看。

    2020-01-05
    2
    5
  • 渺小de尘埃
    当一个结构体里的字段是sync.map类型的,怎么json序列化呢?

    作者回复: 既然要序列化了就用不着同步了吧,用个普通map倒腾一下呗。或者你再包装一下,自定义序列化过程。

    2018-11-01
    3
  • 金时
    // The read field itself is always safe to load, but must only be stored with mu held. 老师,源代码里对read变量注释时说read 在store时,需要加锁,没懂这是为什么?

    作者回复: 你应该知道,atomic.Value 类型的值(以下简称“value”吧)只能保证(完整地)存/取操作的原子性。比如,你在里面存一个 map,它只能保证存这个 map 或取这个 map 的时候是原子操作,但你如果要存、取、改、删这个 map 里的键值对,那 value 就管不到了(这时就会是非原子的操作)。 另外,这里还有一个问题,如果有多个 goroutine 同时向同一个 value 里存值,那么,里面到底存成了哪一个 goroutine 提供的值就不好说了。如果没有并发保护,多个并发写非常容易造成问题。 最后,你看源码肯定也知道,read 里存的是私有类型 readOnly 的实例,这个类型本身并不是并发安全的。所以无论对它做什么操作,都需要并发安全保护。 这也跟“并发多写”的问题有关。 所以,无论是替换 read 里的 readOnly 值(如果是单 goroutine 写就不用,可惜这里不可能做出这种保证),还是修改该值的内部,都需要有锁的保护。 你看,原子操作虽然能保证单值存取的原子性,但还是太简单了,在很多场景下是不足以完全保证并发安全的。不过,作为“可升级的并发安全保障”中的第一级防护还是挺好用的,就像 sync.Map 里做的那样。 sync.Map 的 Store 方法和 Load 方法里都是有一个升级保障的过程的,“升级”的标志就是用了 m.mu 。

    2021-07-10
    2
  • mkii
    老师,看到源码中Store的时候有个疑惑。如果read中存在此key对应的vlaue,则tryStore替换read的value。这里如果在dirty给read并将dirty置为nil的时候不会丢失新数据吗?

    作者回复: 不会啊,这里操作(包括存取和转移)的都是指针啊。

    2021-03-04
    3
    2
  • 夜来寒雨晓来风
    关于文中提到的“键值对应该被删除,但却仍然存在于只读字典”,什么时候会出现这种情形呢?对于sync.Map的删除机制看的不是很明白,希望能解答一下,谢谢~

    作者回复: 虽然在只读字典里,但是你肯定是获取不到的。这样做是为了操作效率,是一种“用空间换时间”的做法。之后内部做整理的时候,这些都会被一并删除的。

    2021-01-07
    2
  • Geek_a8be59
    看了一下源码有地方不理解,有劳解答一下 第一:Store、Load等方法都会执行两次m.read.Load().(readOnly),去判断两次 这样做的目的是什么?

    作者回复: 主要是怕第一次的操作没有体现真实情况,所以在锁的保护下再来一次。这就相当于快路径和慢路径。

    2020-07-21
    2
    2
  • 一介农夫
    1.18后可以用泛型实现了

    作者回复: 嗯嗯,你可以利用泛型改进一下这里的代码。试试看。

    2023-03-06归属地:浙江
    1
  • xl000
    老师,read、dirty交换和访问read这两个操作,难道不需要保护read变量吗

    作者回复: 交换时会有互斥锁的保护,而 read 实际上存在了一个 sync/atomic.Value 里面。所以相关操作都是并发安全的。

    2021-06-21
    1
收起评论
显示
设置
留言
28
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部