一 简言

1.1 文件所在目录:go/src/sync/map.go

1.2 本篇博客的go版本:go1.10.3

二 原理说明

  1. 空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
  2. 使用只读数据(read),避免读写冲突。
  3. 动态调整,miss次数多了之后,将dirty数据提升为read。
  4. double-checking(双重检测)。
  5. 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
  6. 优先从read读取、更新、删除,因为对read的读取不需要锁。
  7. 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 源码分析相关推荐

  1. go语言api源码中文版_Go语言学习——sync.map源码剖析

    1.简介 最近看了下Sync包,详读了sync.map源码,感觉源码实现还是比较巧妙的,有不少可以学习的地方:在讲源码前,先看下sync.map的"历史",从网上搜资料,sync. ...

  2. 【Golang 源码】sync.Map 源码详解

    sync.Map 不安全的 map go 中原生的 map 不是并发安全的,多个 goroutine 并发地去操作一个 map 会抛出一个 panic package main import &quo ...

  3. golang map源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 1. map数据结构 Golang的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个b ...

  4. Go语言——map 源码分析

    之前自己整理的map源码浅析,还是有些不理解的地方,这篇转载曹大的笔记,借鉴下 原文地址:https://github.com/cch123/golang-notes/blob/master/map. ...

  5. go sync.WaitGroup源码分析

    go版本 :1.10.3 原理实现:信号量 信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源. 可简单理解为信号量为一个数值: 当信号量>0时,表示资源可用,获 ...

  6. 【Go】sync.RWMutex源码分析

    RWMutex 读写锁相较于互斥锁有更低的粒度,它允许并发读,因此在读操作明显多于写操作的场景下能减少锁竞争的次数,提高程序效率. type RWMutex struct {w Mutex // he ...

  7. 【Go】sync.WaitGroup 源码分析

    WaitGroup sync.WaitGroup 用于等待一组 goroutine 返回,如: var wg = sync.WaitGroup{}func do() {time.Sleep(time. ...

  8. sync.Map 源码学习

    golang 线程安全的 Map 作者水平有限,而并发博大精深. 如文章中有任何错误, 希望读者不吝指出.谢谢! 章节目录 Map 基本类型定义 Store Load Delete Range Map ...

  9. Golang sync.Mutex源码分析

    sync.Mutex是一个不可重入的排他锁. 这点和Java不同,golang里面的排它锁是不可重入的.当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞 ...

最新文章

  1. sql help cs
  2. hive sql 怎么实现循环_Hive存储过程实现-hpsql
  3. 程序员面试金典——1.8反转子串
  4. validators配置要点及No result defined for action报错解决方案
  5. 谷歌开源的代码评审规范,值得借鉴!
  6. 7.数据结构 --- 图
  7. 寻找大富翁(堆排序)
  8. Spark 广播变量 TorrentBroadcast
  9. Oracle12c用户名scott,Oracle12c新特性pdborcl,如何登录到普通用户scott ?
  10. lvgl8.2 分析画面刷新
  11. m4a录音文件损坏修复_m4a录音文件怎么打开 - 卡饭网
  12. 微信文章如何采集php,记录微信公众号历史文章采集(二、js代码完善和数据库建立)...
  13. 使用百度云主机的GPU主机教程_第二部分
  14. 【iMessage苹果相册推位置推】 去向证书发送到 App Store 本子, 可以使用同一个出站证书,这样可以测试你的ME环境出站工艺流程是不是有题目题目。
  15. python idle使用教程_pythonidle中文教程
  16. zx-editor 移动端(HTML5)富文本编辑器,可与原生App混合(hybrid)开发
  17. 使用gitbook制作电子书
  18. python 发包的方法_python requests 三种发包
  19. linux fdisk调整root大小,Linux下分区大小改变及fdisk应用
  20. 滚筒洗衣机尺寸 2022

热门文章

  1. tab翻页导致的问题
  2. ADSL Modern+无线路由实现无线上网
  3. 联想B450系列安装XP且开启AHCI
  4. Javascript网站繁简转换解决方案
  5. CodeForces - 1521D Nastia Plays with a Tree(树上最小路径覆盖)
  6. CodeForces - 613D Kingdom and its Cities(虚树+贪心)
  7. CodeForces - 427D Match Catch(后缀数组/广义后缀自动机)
  8. TensorFlow2-高层API接口Keras
  9. 在Linux中安装R语言包,遇到无法验证下列签名的错误
  10. 手把手教你玩转SOCKET模型:重叠I/O篇