go sync.map 源码分析
一 简言
1.1 文件所在目录:go/src/sync/map.go
1.2 本篇博客的go版本:go1.10.3
二 原理说明
- 空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
- 使用只读数据(read),避免读写冲突。
- 动态调整,miss次数多了之后,将dirty数据提升为read。
- double-checking(双重检测)。
- 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
- 优先从read读取、更新、删除,因为对read的读取不需要锁。
- read并非仅仅只读,其实更新,删除都是在read中操作的
二 简单使用
三 源码分析
基本结构体的定义
// Map就像一个map[interface{}]interface{},但是是并发安全的
// 针对两种情况进行了优化
// 1. 只写入一次,但多次读取
// 2. 多协程读取,写入,覆盖不同的key
type Map struct {// 锁,访问dirty时才会加锁mu Mutex// read 包含了map中可以安全并发访问的部分(使用或者不使用mu锁)// read 可以总是安全的load,但是store时必须和mu一起使用// read中的entry可以不加锁,并发地更新,但是想要更新一个之前被置为expunged的entry,那么必须加入到dirty中,保持和read一致read atomic.Value // 下面的readOnly对象// dirty包含了map中需加锁才能访问的key// 被置为enxgunged的entry不会保存在dirty中// 若dirty为nil,read中的amended为false;若dirty不为nil,read中的amended为truedirty map[interface{}]*entry// misses 统计了read中读取key不成功,需加锁后判断dirty中是否存在key的次数// 一旦未命中次数和dirty长度一样时,平均每个key未命中一次,也就是说加锁消耗达到了拷贝一次整个dirty,就把dirty提升为readmisses int
}type readOnly struct {m map[interface{}]*entryamended bool // true表dirty中包含read中不存在的key
}// expunged 是一个随意的指针,用来标记dirty map中的一个entry已经被删除,read中一个entry被删除时,是置为nil
var expunged = unsafe.Pointer(new(interface{}))type entry struct {// p指向存储的interface值// 若p为nil,表明该entry在read中被删除了,但dirty中还在,所以能直接更新值// 若p为expunged,表明该entry不存在于dirty中(因为只有dirty新建时,p才有可能被置为expunged,具体见tryExpungeLocked函数),所以更新时要把这个entry复制到dirty中// 其他情况,该entry有效,且记录在read中,若dirty不为空,那么dirty也存在该entry// 一个entry可以通过使用原子操作替换为nil来删除// 一个entry对应的值,可以通过原子操作来替换// 若p为expunged,一个entry的对应值可以被更新,只有在第一次设置m.dirty[key] = e后p unsafe.Pointer // *interface{}
}// 为i(其实是key对应的value值)新建一个entry,其中p保存的是i的地址
func newEntry(i interface{}) *entry {return &entry{p: unsafe.Pointer(&i)}
}
3.1 新建,修改(Store(),LoadOrStore()),源码及分析
// 存储key,value
func (m *Map) Store(key, value interface{}) {// 先从read中读取,存在则尝试存储,成功则返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok && e.tryStore(&value) {return}// read中不存在key,或者尝试存储时失败,加锁m.mu.Lock()// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)read, _ = m.read.Load().(readOnly)if e, ok := read.m[key]; ok { // 若read中存在该key// 若被标记为expunged(函数内已更改为nil),说明dirty中不存在该entry(因为e.p置为expunged只有一处,即dirty时新建/重建时调用的tryExpungeLocked函数,并未添加到dirty中),所以此时需在dirty中添加该keyif e.unexpungeLocked() {m.dirty[key] = e}// 修改entry的值,注意:修改后read,dirty中都被修改了,因为read,dirty中保存的时entry的指针,这里是通过指针修改的e.storeLocked(&value)} else if e, ok := m.dirty[key]; ok { // dirty中存在了,则更新dirty中e.storeLocked(&value)} else { // dirty中也没有该key// 若dirty中不包含read中不存在的key,说明dirty还未新建或重建,此时应创建dirtyif !read.amended {// 新建dirtym.dirtyLocked()// 新建read,并把amended标记为true,因此添加该key后,dirty中就包含了read中不包含的key了m.read.Store(readOnly{m: read.m, amended: true})}// dirty中为该key新建一个entry条目m.dirty[key] = newEntry(value)}m.mu.Unlock()
}// 为该条目尝试存储值i(多协程操作,e.p的值可能有多种情况,所以需for循环使用CAS)
// 若已删除,直接返回false
// 若未删除,则循环使用CAS设置值;即使为nil,也可以赋值
func (e *entry) tryStore(i *interface{}) bool {p := atomic.LoadPointer(&e.p)// 若已删除,则直接返回if p == expunged {return false}// 未删除,则CAS操作,设置该值for {// CAS赋值if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {return true}// 设置失败,则重新获取值p = atomic.LoadPointer(&e.p)// 若已删除,则说明其他协程刚刚删除了该值,这里不可再设置if p == expunged {return false}}
}// 为该entry无条件存储值i(此时mu锁是锁定的,函数Locked结尾即标明;调用时e.p不能是expunged,使用原子操作赋值)
func (e *entry) storeLocked(i *interface{}) {atomic.StorePointer(&e.p, unsafe.Pointer(i))
}// unexpungeLocked 判断本entry是否为expunged状态(此时mu锁是锁定的,判断规则见下面)
// 若该entry的value为expunged,说明已不在dirty中(因为e.p置为expunged只有一处,即dirty时新建时调用的tryExpungeLocked函数,那里并未复制到dirty中),
// 在mu锁解锁之前,必须要重新加入到dirty中,这里置为nil,返回true
// 若该entry的value不为expunged,则返回false
func (e *entry) unexpungeLocked() (wasExpunged bool) {return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}// 新建dirty(此时mu锁是锁定的)
func (m *Map) dirtyLocked() {if m.dirty != nil {return}// 获取readread, _ := m.read.Load().(readOnly)// 新建dirtym.dirty = make(map[interface{}]*entry, len(read.m))// 遍历read,逐个值拷贝给dirty// 不能直接整个map赋值,是因为read中可能有些entry已经被标记为删除(为nil),我们只需复制那些未被删除的// 注意:若entry为nil,则置为expunged,标记为彻底删除,不再复制到dirty中for k, e := range read.m {if !e.tryExpungeLocked() {m.dirty[k] = e}}
}// 尝试删除本entry(此时mu锁是锁定的)
// 返回值表本entry是否已删除
func (e *entry) tryExpungeLocked() (isExpunged bool) {// 原子地重新读取p := atomic.LoadPointer(&e.p)// 指针为nil时(此时mu是锁定的,为什么还要for循环来尝试CAS呢,不懂?????)for p == nil {// 用CAS操作,置为expunged,以此来标记本条目已被彻底删除,成功则返回if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {return true}// CAS失败后再获取存储的指针,若为nil,则一直尝试;若不为nil了,则跳出p = atomic.LoadPointer(&e.p)}// 若为expunged说明,已被标记删除,否则,未删除return p == expunged
}// LoadOrStore 若key存在,则返回对应的value,loaded为true
// 若key不存在,则存储给定的value值;loaded为false
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// 先尝试从read中读取,若read中存在,则尝试loadorstore,成功则返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}// read中不存在该key,或存在但tryLoadOrStore失败,则加锁m.mu.Lock()// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)read, _ = m.read.Load().(readOnly)// read中已存在该keyif e, ok := read.m[key]; ok {// 若为expunged状态,说明dirty中已被删除,需添加到dirty中if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的} else if e, ok := m.dirty[key]; ok { // dirty中存在该key// 在dirty中尝试loadorstoreactual, loaded, _ = e.tryLoadOrStore(value)// 未命中时的处理m.missLocked()} else { // dirty中也不存在该key// 若此时dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),说明是第一次新加key到dirty中if !read.amended {// 建立dirtym.dirtyLocked()// amended 置为true,因为此次添加key后,dirty中就包含了read中不存在的keym.read.Store(readOnly{m: read.m, amended: true})}// dirty中新建该keym.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded
}
注意事项:
1. Store()时,若read中有,且不为expunged,则优先在read中修改
2. 若read中不存在,dirty中存在,则修改dirty中的;若dirty中不存在,可能需要新建/重建dirty,此时amended为true
3. Store() 可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测
3.2 读取(Load(),LoadOrStore()), 源码及分析
// 读取key的值
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {read, _ := m.read.Load().(readOnly)// 先尝试从read中读取e, ok := read.m[key]// read中不存在,且dirty中包含read中不存在的keyif !ok && read.amended {// 加锁m.mu.Lock()// 双重检测(因为加锁过程中,可能dirty被提升为read,dirty被置为nil,此时read中可能有该key了)read, _ = m.read.Load().(readOnly)e, ok = read.m[key]// read中仍然不存在该key,且dirty中包含read中不存在的keyif !ok && read.amended {e, ok = m.dirty[key]// 无论dirty中是否存在该key,都要记录一次未命中,然后根据情况,决定是否把dirty提升为readm.missLocked()}m.mu.Unlock()}// 仍然不存在,返回if !ok {return nil, false}// 通过entry加载数据return e.load()
}// 从entry中加载值
// 值为nil,expunged时,说明已经被删除
func (e *entry) load() (value interface{}, ok bool) {// 原子地重新读取值p := atomic.LoadPointer(&e.p)if p == nil || p == expunged {return nil, false}// 先转换为接口指针,再取值return *(*interface{})(p), true
}// 未命中时的处理(此时mu锁是锁定的)
func (m *Map) missLocked() {m.misses++// 未命中次数没有达到dirty时,返回,即平均每个key达到未命中一次if m.misses < len(m.dirty) {return}// 把dirty提升为read,直接整个map赋值,里面的amended未赋值,即默认false,表dirty中不包含read中不存在的key,因为下面dirty就要被置为nil了m.read.Store(readOnly{m: m.dirty})m.dirty = nilm.misses = 0
}// LoadOrStore 若key存在,则返回对应的value,loaded为true
// 若key不存在,则存储给定的value值;loaded为false
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// 先尝试从read中读取,若read中存在,则尝试loadorstore,成功则返回read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}// read中不存在该key,或存在但tryLoadOrStore失败,则加锁m.mu.Lock()// 双重检测(因为加锁过程中,dirty可能被提升为read,dirty被置为nil,此时read中可能存在该key了)read, _ = m.read.Load().(readOnly)// read中已存在该keyif e, ok := read.m[key]; ok {// 若为expunged状态,说明dirty中已被删除,需添加到dirty中if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value) // 更新后read,dirty中都是最新的} else if e, ok := m.dirty[key]; ok { // dirty中存在该key// 在dirty中尝试loadorstoreactual, loaded, _ = e.tryLoadOrStore(value)// 未命中时的处理m.missLocked()} else { // dirty中也不存在该key// 若此时dirty中不包含read中不存在的key,(即dirty中的key在read中都存在),说明是第一次新加key到dirty中if !read.amended {// 建立dirtym.dirtyLocked()// amended 置为true,因为此次添加key后,dirty中就包含了read中不存在的keym.read.Store(readOnly{m: read.m, amended: true})}// dirty中新建该keym.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded
}// tryLoadOrStore(多协程操作,此时mu锁未锁定)
// 若该条目未被删除,原子地读取value值,loaded=true,ok=true
// 若该条目已被删除(expunged或nil),则tryLoadOrStore函数不修改,直接返回,loaded==false,ok==false
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {p := atomic.LoadPointer(&e.p)// 已被删除,dirty中不存在该entry,不可赋值if p == expunged {return nil, false, false}// 正常有效值,则返回其值if p != nil {return *(*interface{})(p), true, true}// 下面是为nil值的情况// Copy the interface after the first load to make this method more amenable// to escape analysis: if we hit the "load" path or the entry is expunged, we// shouldn't bother heap-allocating.ic := ifor {// CAS操作,赋值if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {return i, false, true}p = atomic.LoadPointer(&e.p)// 已被删除,说明其他协程刚刚删除了,返回失败if p == expunged {return nil, false, false}// 正常值,返回成功if p != nil {return *(*interface{})(p), true, true}}
}
注意事项:
1. Load()时,若read中存在,直接读取值,返回
2. 若read中不存在,则未命中次数+1,累计次数达到dirty长度时,也就是平均每个key达到一次未命中,则把dirty提升为read
3. Load(),LoadOrStore() 可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测
3.3 删除(Delete()), 源码及分析
// 删除一个key
func (m *Map) Delete(key interface{}) {// 优先从read中读取read, _ := m.read.Load().(readOnly)e, ok := read.m[key]// read中不存在,但dirty中包含read中不存在的key时,需要从dirty中删除if !ok && read.amended {// 必加锁m.mu.Lock()// 双重检测(因为加锁的过程中dirty可能被提升为read,dirty被置为nil,此时read中可能有该key了)read, _ = m.read.Load().(readOnly)e, ok = read.m[key]// 仍然不存在,但dirty中包含read中不存在的key时,从dirty中删除(在锁定中,所以可以真正删除)if !ok && read.amended {delete(m.dirty, key)}m.mu.Unlock()}// 存在时,一定是存在于read中,只需通过entry标记为已删除(置为nil)if ok {e.delete()}
}// 标记一个条目删除,置为nil(mu锁未加锁,需考虑多协程安全,本entry此时存在于read中)
func (e *entry) delete() (hadValue bool) {for {// 原子地重新读取p := atomic.LoadPointer(&e.p)// nil 表已被其他协程通过Delete()标记删除,不再操作// expunged 表已被其他协程提升dirty为read时,标记删除,不再操作if p == nil || p == expunged {return false}// CAS操作,置为nilif atomic.CompareAndSwapPointer(&e.p, p, nil) {return true}}
}
注意事项:
1. Delete()时,若read中存在,直接在read中标记为删除(置nil),并非真正的立即删掉,这样可以下次赋值时直接操作,达到重复使用的目的
2. 若read中存在key,标记删除后,此时dirty里面的该key是不准确的,这种不准确是临时的,因为每次往dirty中新加入key的时候,会先判断dirty是否为nil,为空则新建,且从read中筛选一遍,这样dirty又是准确的了;以后dirty被提升为read时,为nil的会被置为expunged,来标记为彻底删除,不会压入到dirty中
3. 若dirty中存在key,则彻底删除掉
4. Delete()可以多协程环境下调用,需多注意并发问题,比如判断read是否有key的双重检测,比如置为nil时的for循环CAS操作
5. 切记:dirty为nil 和 read.amended 为false,是同时存在的
要么dirty = nil,read.amended = false,要么dirty != nil,amended = true
go sync.map 源码分析相关推荐
- go语言api源码中文版_Go语言学习——sync.map源码剖析
1.简介 最近看了下Sync包,详读了sync.map源码,感觉源码实现还是比较巧妙的,有不少可以学习的地方:在讲源码前,先看下sync.map的"历史",从网上搜资料,sync. ...
- 【Golang 源码】sync.Map 源码详解
sync.Map 不安全的 map go 中原生的 map 不是并发安全的,多个 goroutine 并发地去操作一个 map 会抛出一个 panic package main import &quo ...
- golang map源码分析
2019独角兽企业重金招聘Python工程师标准>>> 1. map数据结构 Golang的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个b ...
- Go语言——map 源码分析
之前自己整理的map源码浅析,还是有些不理解的地方,这篇转载曹大的笔记,借鉴下 原文地址:https://github.com/cch123/golang-notes/blob/master/map. ...
- go sync.WaitGroup源码分析
go版本 :1.10.3 原理实现:信号量 信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源. 可简单理解为信号量为一个数值: 当信号量>0时,表示资源可用,获 ...
- 【Go】sync.RWMutex源码分析
RWMutex 读写锁相较于互斥锁有更低的粒度,它允许并发读,因此在读操作明显多于写操作的场景下能减少锁竞争的次数,提高程序效率. type RWMutex struct {w Mutex // he ...
- 【Go】sync.WaitGroup 源码分析
WaitGroup sync.WaitGroup 用于等待一组 goroutine 返回,如: var wg = sync.WaitGroup{}func do() {time.Sleep(time. ...
- sync.Map 源码学习
golang 线程安全的 Map 作者水平有限,而并发博大精深. 如文章中有任何错误, 希望读者不吝指出.谢谢! 章节目录 Map 基本类型定义 Store Load Delete Range Map ...
- Golang sync.Mutex源码分析
sync.Mutex是一个不可重入的排他锁. 这点和Java不同,golang里面的排它锁是不可重入的.当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞 ...
最新文章
- sql help cs
- hive sql 怎么实现循环_Hive存储过程实现-hpsql
- 程序员面试金典——1.8反转子串
- validators配置要点及No result defined for action报错解决方案
- 谷歌开源的代码评审规范,值得借鉴!
- 7.数据结构 --- 图
- 寻找大富翁(堆排序)
- Spark 广播变量 TorrentBroadcast
- Oracle12c用户名scott,Oracle12c新特性pdborcl,如何登录到普通用户scott ?
- lvgl8.2 分析画面刷新
- m4a录音文件损坏修复_m4a录音文件怎么打开 - 卡饭网
- 微信文章如何采集php,记录微信公众号历史文章采集(二、js代码完善和数据库建立)...
- 使用百度云主机的GPU主机教程_第二部分
- 【iMessage苹果相册推位置推】 去向证书发送到 App Store 本子, 可以使用同一个出站证书,这样可以测试你的ME环境出站工艺流程是不是有题目题目。
- python idle使用教程_pythonidle中文教程
- zx-editor 移动端(HTML5)富文本编辑器,可与原生App混合(hybrid)开发
- 使用gitbook制作电子书
- python 发包的方法_python requests 三种发包
- linux fdisk调整root大小,Linux下分区大小改变及fdisk应用
- 滚筒洗衣机尺寸 2022
热门文章
- tab翻页导致的问题
- ADSL Modern+无线路由实现无线上网
- 联想B450系列安装XP且开启AHCI
- Javascript网站繁简转换解决方案
- CodeForces - 1521D Nastia Plays with a Tree(树上最小路径覆盖)
- CodeForces - 613D Kingdom and its Cities(虚树+贪心)
- CodeForces - 427D Match Catch(后缀数组/广义后缀自动机)
- TensorFlow2-高层API接口Keras
- 在Linux中安装R语言包,遇到无法验证下列签名的错误
- 手把手教你玩转SOCKET模型:重叠I/O篇