北京公司注册网站,网站图标添加,商丘网红宋飞,短视频运营1.简介 最近看了下Sync包#xff0c;详读了sync.map源码#xff0c;感觉源码实现还是比较巧妙的#xff0c;有不少可以学习的地方#xff1b;在讲源码前#xff0c;先看下sync.map的历史#xff0c;从网上搜资料#xff0c;sync.map是Go语言在1.9版本才引入…1.简介 最近看了下Sync包详读了sync.map源码感觉源码实现还是比较巧妙的有不少可以学习的地方在讲源码前先看下sync.map的历史从网上搜资料sync.map是Go语言在1.9版本才引入的并发安全的map对此有些同学心中可能会有个疑问如果是支持并发为什么不采取锁map的方式为啥还要在单独搞个sync.map结构呢我们先看下锁map存在的问题参考go语言中文文档www.topgoer.com转自https://studygolang.com/topics/12363#reply0 1)mutex map 最简单的方案就是在map上加个锁针对map的所有操作都要提前加锁其存在问题也很明显锁竞争会非常频繁 2)rwmutex map 优化一点依据场景如果是读操作多于写操作可以把mutex换成rwmutex相比方案一有一定优化、至少读读之间不会存在互斥不过读写之间还会存在阻塞 根据锁map的优化迭代方案可知在读读场景下rwmutex map可以并发、不存在阻塞但是读写还是存在阻塞而sync.map要做的事情就是能进一步优化对于map的各种操作尽可能不阻塞为此sync.map采用了两级缓存实现一级缓存做无锁并发二级缓存做有锁并发如 对上图说明两点 1)针对sync.map的各种操作都先经过一级缓存一级缓存采用无锁的方式只要不出现击穿即key都在一级缓存中可以找到则就不会访问到二级缓存 2)一级缓存和二级缓存之间存在数据同步二级缓存数据相对更全一些所以当一级缓存数据比较久时可以将二级缓存数据同步一下该情况是在读击穿时处理在不击穿的前提下一级缓存中可能有数据删除数据移除情况也要同步给二级缓存清除废弃数据、减少空间占用该情况是在写击穿并且是一、二级缓存都不存在键的情况处理总之同步的原则是一级缓存数据尽可能新一级缓存数据只能是二级缓存的子集2.实现 sync.map的优势是理想情况下以无锁代替有锁、提高性能但存在击穿后不得不加锁的问题一旦击穿进入二级缓存就要进行锁操作了所以sync.map不太适用于写多读少以及频繁创建新键的情况因为要考虑击穿问题所以sync.map的实现也是围绕击穿考虑的。 2.1读操作 读操作比较简单步骤是 1)查看一级缓存中是否有key有就返回对应value 2)如果没有则进入读击穿加锁后在复看一级缓存中是否有key(复看是因为存在二级缓存向一级缓存同步数据的情况)有就返回对应value 3)如果没有则看二级缓存中有没有有就返回对应value此时出现读击穿会进入读击穿保护机制——击穿达到一定次数会将二级缓存数据同步到一级缓存 需要注意的是在返回value时要检测value的有效性如果已经废弃(expunged状态)则不用返回。2.2写操作 写操作相对复杂根据key是否存在的情况可以分为create和update步骤是 1)查看一级缓存中是否有key有就尝试更新之所以是尝试是因为还要检查数据是否已经废弃如果已经废弃即使key在一级缓存中存在也是击穿效果因为二级缓存中没有 2)如果一级缓存操作失败加锁后在复看一级缓存如果有key则更新value并检测value是否为废弃状态如果是则将key、value写入二级缓存 3)如果一级缓存中一直没有key但二级缓存中有则直接更新数据 4)如果一级缓存和二级缓存都没有key则将key、value写入二级缓存此时会尝试将一级缓存数据同步给二级缓存用于删除废弃数据(将一级缓存中的删除数据设置为expunged状态)因为只有该情况下一级缓存数据可能是二级缓存数据的子集所以当插入全新的key时才会尝试更新缓存数据、移除废弃数据2.3删除操作 删除采取的是延迟删除操作对于待删除数据其value先设置为nil优先从一级缓存删除如果一级缓存没有再去二级缓存中删除。2.4源码 以1.14.4版本为例处理源码是func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, _ : m.read.Load().(readOnly) e, ok : read.m[key] // 一级缓存没有查到key加锁、复查amended用于判断一级缓存和二级缓存是否一致 if !ok read.amended { m.mu.Lock() read, _ m.read.Load().(readOnly) e, ok read.m[key] // 一级缓存还是没有查到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) Store(key, value interface{}) { 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 { // 一级缓存中有key则更新value同时还要查看value是否已经废弃如果废弃还要将数据写入二级缓存确保下次同步前二级缓存数据的完整性因为操作到二级缓存所以需要放在锁操作下这也是为什么tryStore只是尝试存储 if e.unexpungeLocked() { m.dirty[key] e } e.storeLocked(value) } else if e, ok : m.dirty[key]; ok { e.storeLocked(value) } else { // 对于key完全不存在的情况尝试数据同步从一级缓存到二级缓存 if !read.amended { m.dirtyLocked() // 数据同步时废弃数据不会同步废弃数据会设置为expunged状态 m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] newEntry(value) } m.mu.Unlock()}// Delete部分相对简单主要是将value设置为nilfunc (m *Map) Delete(key interface{}) { 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 { delete(m.dirty, key) } m.mu.Unlock() } if ok { e.delete() }}func (e *entry) delete() (hadValue bool) { for { p : atomic.LoadPointer(e.p) if p nil || p expunged { return false } if atomic.CompareAndSwapPointer(e.p, p, nil) { return true } }}3.总结 sync.map是以无锁操作一级缓存的方式支持并发、提高性能而根据其实现可知sync.map适用于读多、更新多、新建少的场景(新建情况下可能会带来较大的开销比如读击穿、数据刚从二级缓存同步到一级缓存后又要新建key数据又要反向同步一次)。