35 | 并发安全字典sync.Map (下)
35 | 并发安全字典sync.Map (下)
讲述:黄洲君
时长11:12大小6.41M
知识扩展
问题 1:怎样保证并发安全字典中的键和值的类型正确性?(方案二)
问题 2:并发安全字典如何做到尽量避免使用锁?
总结
思考题
赞 14
提建议
精选留言(25)
- Timo2019-05-30做个sync.Map优化点的小总结: 1. 空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响 2. 使用只读数据(read),避免读写冲突。 3. 动态调整,miss次数多了之后,将dirty数据提升为read 4. 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据 5. 优先从read读取、更新、删除,因为对read的读取不需要锁展开共 1 条评论52
- sky2018-11-01郝大 go方面能推荐下比较成熟的微服务框架吗
作者回复: 在我发布的Github优秀Go语言项目的思维导图里有。
共 2 条评论12 - 下雨天2020-10-20老师好,关于:sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。这句话,只读map里面的值可以被替换的话,为什么不需要加锁?不会 有读写冲突吗?
作者回复: 【第一个层面】 因为这里面的只读字典 read 是通过 sync/atomic.Value 存储的,又正因为 read 是只读的,不存在增删键值对的情况,所以从 read 整体的层面可以安全地操作其中的某个键值对。 【第一个层面:更具体的细节】 在这种保护下,只要这里的键值对在数量上没有增减,就不会出现新数据丢失或者弄脏数据的情况。反例的话,可以看我最近写的这篇文章:https://mp.weixin.qq.com/s/ru161EtyQMrQVtWji0CqzQ 【第二个层面】 由于其中的每一个键值对(entry 结构)都可以保证自身的并发性( entry 内部只会存指针,因此用原子操作就可以保证并发读写的安全性),所以从 read 中单个键值对的层面也就有了并发安全性方面的保证。 【结论】 综合以上两种措施,这里才无需使用锁来保护。
5 - 我来也2020-08-26最近几天,golang升级到了1.15后,sync.Map又`火`了一把. 我又来温习了下本篇文章,也看了下对应的源码, 对sync.Map的印象又深刻了点. 具体的链接如下: [踩了 Golang sync.Map 的一个坑](https://gocn.vip/topics/10860) [sync: sync.Map keys will never be garbage collected](https://github.com/golang/go/issues/40999) 有兴趣的小伙伴可以去看看.展开6
- 疯琴2020-01-05这个设计很巧妙,在自己的开发中可以借鉴这种思想。有个问题请问老师: “脏字典中的键值对集合总是完全的”,而“read 和 dirty 互换之后 dirty 会置空”,那么重建的意思是不是这样的:在下一次访问 read 的时候,将 read 中的键值对全部复制到 dirty 中?
作者回复: read 和 dirty 互换是分两步走的。Load 的时候如果发现“不得不去 dirty 中查找”的情况已经有很多了,就会把 dirty 作为新的 read,然后把 dirty 置为 nil。之后,在 Store 的时候,如果发现健是新的,而且是对于新 read 的第一个新健(此时 dirty 必定为 nil),那么就重新初始化 dirty,然后把新 read 中的有效键一个一个地存入 dirty。 另外你可以尝试阅读一下 sync.Map 的源码,写得还是挺清晰的。可以配合着这里的文章去看。
共 2 条评论5 - Rainman2019-03-09这个脏字典让我想起了mysql的刷脏页。 给老师点赞。5
- 大漠胡萝卜2020-07-12sync.Map适用于读多写少的情况,如果写数据比较频繁可以参考:https://github.com/orcaman/concurrent-map4
- 墨水里的鱼2018-11-30如何初始化reflect.Type?reflect.TypeOf(1) reflect.TypeOf("a") 只能这样吗?3
- 渺小de尘埃2018-11-01当一个结构体里的字段是sync.map类型的,怎么json序列化呢?
作者回复: 既然要序列化了就用不着同步了吧,用个普通map倒腾一下呗。或者你再包装一下,自定义序列化过程。
3 - 金时2021-07-10// 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 。
2 - mkii2021-03-04老师,看到源码中Store的时候有个疑惑。如果read中存在此key对应的vlaue,则tryStore替换read的value。这里如果在dirty给read并将dirty置为nil的时候不会丢失新数据吗?
作者回复: 不会啊,这里操作(包括存取和转移)的都是指针啊。
共 3 条评论2 - 夜来寒雨晓来风2021-01-07关于文中提到的“键值对应该被删除,但却仍然存在于只读字典”,什么时候会出现这种情形呢?对于sync.Map的删除机制看的不是很明白,希望能解答一下,谢谢~
作者回复: 虽然在只读字典里,但是你肯定是获取不到的。这样做是为了操作效率,是一种“用空间换时间”的做法。之后内部做整理的时候,这些都会被一并删除的。
2 - Geek_a8be592020-07-21看了一下源码有地方不理解,有劳解答一下 第一:Store、Load等方法都会执行两次m.read.Load().(readOnly),去判断两次 这样做的目的是什么?
作者回复: 主要是怕第一次的操作没有体现真实情况,所以在锁的保护下再来一次。这就相当于快路径和慢路径。
共 2 条评论2 - xl0002021-06-21老师,read、dirty交换和访问read这两个操作,难道不需要保护read变量吗
作者回复: 交换时会有互斥锁的保护,而 read 实际上存在了一个 sync/atomic.Value 里面。所以相关操作都是并发安全的。
1 - simple_孙2021-05-27更新时只修改只读map,不会造成数据不一致吗,后面应该会定期同步到脏map里吧?
作者回复: 不会造成数据不一致,因为读的时候会先读 read 再读 dirty。而且不是“只修改 read”,如果 read 里面有这个键,那就修改 read,如果没有还会到 dirty 里面找。 sync.Map 会定期让 dirty 变成 read,然后重建 dirty(以下简称“read 转换”吧)。“定期”的时机是,dirty 中有太多 read 里没有的键值对(以下简称“脏键值对”吧)。 具体来说是,发现一个“脏键值对”就增加一次计数。每当已发现的“脏键值对”的数量等于或大于 dirty 的长度时,就触发一次“read 转换”。
1 - Calios2019-08-14个人以为是读过的这个专栏里最精彩的一篇~ 只读字典和脏字典的实现好精彩,忍不住再去细细读一下sync.Map的实现。1
- 唐大少在路上。。。2019-05-11班门弄斧,其实有个细节帮老师丰富一下: 两个原生map的定义为 map[interface{}]*entry 其中的entry为一个只包含一个unsafe.pointer的结构体 这里之所以value设置为指针类型,个人感觉就是为了在dirtry重建的时候直接把read里面这个entry的地址copy到dirty里面,这样当read中对entry里面的pointer执行原子替换的时候,dirty里面的值也会跟随着改变。 这样,当每次对read已有key进行更新的时候就不用单独去操作一次dirty了展开1
- 虢國技醬2019-03-03打卡1
- Ronin2022-10-28 来自北京老师,我读了好几遍,还是不明白为什么要检查键值对的类型,这不是强制整个字典的key都为一个类型,整个字典的value都为一个类型了,而实际上原先的字典的键值对类型是可以多样化的存储,像这样: var m sync.Map m.Store("test", 18) m.Store(18, "test") 而不是只能: m.Store(1, "a") m.Store(2, "b") 就算要检查,应该是检查键的实际类型不能是函数类型、字典类型和切片类型 还望老师解惑下~谢谢展开
作者回复: sync.Map 依然是可以多类型存储的啊,你觉得这是优势还是劣势呢? 我觉得这是并不是什么好事。因为这相当于把检查类型的工作抛给了程序员。实际上,这在当初也是一种在缺少自定义泛型支持情况下的 plan B,并不是最佳实践。 现在有了泛型,sync.Map其实可以被改造的更好,只不过为了向后兼容性,折中改造就被搁置了。
- Jason2022-10-11 来自北京郝大,map的键值对的删除为什么要先置为nil再置为unpunged呢,直接置为unpunged不行吗?而且真正删除一个键值对要经过delete->dirtyLocked->missLocked三个步骤才能删除
作者回复: 为rehash做准备啊,防止到时候内部状态不一致,rehash在map中是非常重要的一环,它的很多内部状态都是与rehash相关的。另外也是为了防止内存泄露。