Golang sync.Map 深入探究

张开发
2026/5/31 5:14:05 15 分钟阅读
Golang sync.Map 深入探究
欢迎各位同学关注我哦~在这个 AI 喧嚣的时代不忘初心戒骄戒躁认真沉淀Go 1.9 引入了sync.Map为并发场景提供了一种高性能的 map 实现。但它的设计思路与普通的mapsync.RWMutex截然不同这篇文章从源码层面拆解它的实现原理。一、为什么需要 sync.Map普通的map在并发读写时会 panic必须配合锁使用varmu sync.RWMutexvarmmake(map[string]int)// 读mu.RLock()v:m[key]mu.RUnlock()// 写mu.Lock()m[key]1mu.Unlock()RWMutex在读多写少的场景下性能不错但有两个问题写锁阻塞读锁即使写的是不同的 key也会阻塞所有读操作锁竞争高并发下锁成为瓶颈sync.Map的设计目标是场景sync.Map 优势key 只写一次多次读取读操作无锁直接走 atomic不同 goroutine 操作不同 key减少锁竞争提高并行度二、核心数据结构2.1 Map 结构// sync/map.gotypeMapstruct{mu Mutex// read 包含 map 中可以安全并发访问的部分// 总是可以安全加载但存储时必须持有 muread atomic.Value// readOnly// dirty 包含 map 中需要持有 mu 才能访问的部分// 为了快速提升为 read它也包含 read 中所有未被 expunged 的 entrydirtymap[any]*entry// misses 统计从 read 读取失败需要加锁访问 dirty 的次数// 当 misses 达到一定阈值dirty 会被提升为 readmissesint}2.2 readOnly 结构typereadOnlystruct{mmap[any]*entry amendedbool// dirty 包含 read.m 中没有的 key}2.3 entry 结构// expunged 是一个特殊指针标记 entry 已从 dirty 中删除varexpungedunsafe.Pointer(new(any))typeentrystruct{// p 指向存储的值有三种状态// nil: 已删除dirty nil 或 dirty[key] e// expunged: 已删除dirty ! nil但 entry 不在 dirty 中// 其他: 有效值同时存在于 read.m[key] 和 dirty[key]p unsafe.Pointer// *interface{}}entry 的三种状态p 值含义在 dirty 中nil已删除可能存在expunged已删除且被清理不存在有效指针有效值存在2.4 整体架构┌─────────────────────────────────────────────────────────────┐ │ Map │ ├─────────────────────────────────────────────────────────────┤ │ mu (Mutex) │ ├─────────────────────────────────────────────────────────────┤ │ read (atomic.Value) ──► readOnly │ │ ├─ m: map[any]*entry │ │ └─ amended: bool │ ├─────────────────────────────────────────────────────────────┤ │ dirty: map[any]*entry │ │ (包含 read 中未 expunged 的 entry 新增的 entry) │ ├─────────────────────────────────────────────────────────────┤ │ misses: int │ └─────────────────────────────────────────────────────────────┘三、Load读操作func(m*Map)Load(key any)(value any,okbool){// 1. 从 read 读取无锁read,_:m.read.Load().(readOnly)e,ok:read.m[key]// 2. read 没有但 amended 为 true说明 dirty 可能有if!okread.amended{m.mu.Lock()// 双重检查加锁期间可能 dirty 已提升为 readread,_m.read.Load().(readOnly)e,okread.m[key]if!okread.amended{// 3. 从 dirty 读取e,okm.dirty[key]// 记录 missm.missLocked()}m.mu.Unlock()}if!ok{returnnil,false}// 4. 从 entry 加载值returne.load()}func(e*entry)load()(value any,okbool){p:atomic.LoadPointer(e.p)ifpnil||pexpunged{returnnil,false}return*(*any)(p),true}关键点快路径先从read无锁读取命中则直接返回慢路径read未命中且amended为 true加锁查dirtymiss 计数每次从dirty读取成功都会调用missLocked()3.1 missLockedmiss 计数与 dirty 提升func(m*Map)missLocked(){m.misses// misses 达到 dirty 长度时提升 dirty 为 readifm.misseslen(m.dirty){return}m.read.Store(readOnly{m:m.dirty})m.dirtynilm.misses0}提升时机当 miss 次数等于dirty的元素数量时。这意味着如果dirty的元素经常被访问就会很快提升之后的读取就变成无锁了。四、Store写操作func(m*Map)Store(key,value any){// 1. 快路径read 中已存在该 key尝试 CAS 更新read,_:m.read.Load().(readOnly)ife,ok:read.m[key];oke.tryStore(value){return}m.mu.Lock()read,_m.read.Load().(readOnly)// 2. read 中存在该 keyife,ok:read.m[key];ok{// 如果 entry 是 expunged需要恢复到 dirtyife.unexpungeLocked(){m.dirty[key]e}e.storeLocked(value)}elseife,ok:m.dirty[key];ok{// 3. dirty 中存在该 key直接更新e.storeLocked(value)}else{// 4. 新 keyif!read.amended{// dirty 为 nil需要初始化m.dirtyLocked()m.read.Store(readOnly{m:read.m,amended:true})}m.dirty[key]newEntry(value)}m.mu.Unlock()}4.1 tryStoreCAS 更新func(e*entry)tryStore(i*any)bool{for{p:atomic.LoadPointer(e.p)// 如果是 expunged需要加锁处理ifpexpunged{returnfalse}// CAS 更新ifatomic.CompareAndSwapPointer(e.p,p,unsafe.Pointer(i)){returntrue}}}4.2 unexpungeLocked恢复 expunged 状态func(e*entry)unexpungeLocked()(wasExpungedbool){returnatomic.CompareAndSwapPointer(e.p,expunged,nil)}4.3 dirtyLocked初始化 dirtyfunc(m*Map)dirtyLocked(){ifm.dirty!nil{return}read,_:m.read.Load().(readOnly)m.dirtymake(map[any]*entry,len(read.m))fork,e:rangeread.m{// expunged 的 entry 不加入 dirtyif!e.tryExpungeLocked(){m.dirty[k]e}}}func(e*entry)tryExpungeLocked()(isExpungedbool){p:atomic.LoadPointer(e.p)forpnil{// nil 转换为 expungedifatomic.CompareAndSwapPointer(e.p,nil,expunged){returntrue}patomic.LoadPointer(e.p)}returnpexpunged}关键逻辑新建dirty时遍历read.m将nil状态的 entry 标记为expunged只有非expunged的 entry 才加入dirty五、Delete删除操作func(m*Map)Delete(key any){m.LoadAndDelete(key)}func(m*Map)LoadAndDelete(key any)(value any,loadedbool){read,_:m.read.Load().(readOnly)e,ok:read.m[key]if!okread.amended{m.mu.Lock()read,_m.read.Load().(readOnly)e,okread.m[key]if!okread.amended{e,okm.dirty[key]// 从 dirty 中删除delete(m.dirty,key)m.missLocked()}m.mu.Unlock()}ifok{returne.delete()}returnnil,false}func(e*entry)delete()(value any,okbool){for{p:atomic.LoadPointer(e.p)ifpnil||pexpunged{returnnil,false}// CAS 设置为 nilifatomic.CompareAndSwapPointer(e.p,p,nil){return*(*any)(p),true}}}删除的两种情况场景操作entry 在 read 中CAS 将 p 设为 nilentry 只在 dirty 中直接 delete(m.dirty, key)六、Range遍历操作func(m*Map)Range(ffunc(key,value any)bool){read,_:m.read.Load().(readOnly)ifread.amended{// dirty 有新数据先提升为 readm.mu.Lock()read,_m.read.Load().(readOnly)ifread.amended{readreadOnly{m:m.dirty}m.read.Store(read)m.dirtynilm.misses0}m.mu.Unlock()}// 遍历 read.mfork,e:rangeread.m{v,ok:e.load()if!ok{continue}if!f(k,v){break}}}Range 会触发dirty提升为read保证遍历到所有数据。七、LoadOrStore原子操作func(m*Map)LoadOrStore(key,value any)(actual any,loadedbool){// 快路径read,_:m.read.Load().(readOnly)ife,ok:read.m[key];ok{actual,loaded,ok:e.tryLoadOrStore(value)ifok{returnactual,loaded}}m.mu.Lock()// ... 类似 Store 的逻辑}func(e*entry)tryLoadOrStore(i any)(actual any,loaded,okbool){p:atomic.LoadPointer(e.p)ifpexpunged{returnnil,false,false}ifp!nil{// 已有值返回return*(*any)(p),true,true}// p nil尝试存储ic:ifor{ifatomic.CompareAndSwapPointer(e.p,nil,unsafe.Pointer(ic)){returni,false,true}patomic.LoadPointer(e.p)ifpexpunged{returnnil,false,false}ifp!nil{return*(*any)(p),true,true}}}八、状态转换图entry 状态转换 新建 Store │ ▼ ┌─────────┐ Store 已有 key ┌─────────┐ │ 有效值 │◄─────────────────────│ 有效值 │ └────┬────┘ └─────────┘ │ ▲ │ Delete │ Store ▼ │ ┌───┐ ┌─────┴─────┐ │nil│◄────────────────────────│unexpunge │ └─┬─┘ expunge │(加锁时) │ │ dirtyLocked() └───────────┘ │ ▲ ▼ │ ┌──────────┐ Store 已有 key ┌──────┴──────┐ │ expunged │─────────────────►│ nil │ └──────────┘ (加锁 unexpunge) └─────────────┘九、流程图9.1 Load 流程Load(key) │ └─► read.m[key] 存在 │ ├─► 是 ──► entry.load() ──► 返回 │ └─► 否 ──► read.amended │ ├─► false ──► 返回 nil, false │ └─► true ──► 加锁 │ ├─► 双重检查 read.m[key] │ ├─► dirty[key] 存在 │ │ │ ├─► 是 ──► missLocked() │ │ │ └─► 否 ──► missLocked() │ └─► 解锁 ──► entry.load()9.2 Store 流程Store(key, value) │ ├─► read.m[key] 存在 │ │ │ ├─► 是 ──► tryStore 成功 │ │ │ │ │ ├─► 是 ──► 返回 │ │ │ │ │ └─► 否 ──► 继续加锁 │ │ │ └─► 否 ──► 继续加锁 │ └─► 加锁 │ ├─► read.m[key] 存在 │ │ │ ├─► 是 ──► unexpungeLocked() │ │ │ │ │ ├─► 是 expunged ──► 加入 dirty │ │ │ │ │ └─► storeLocked() │ │ │ ├─► dirty[key] 存在 │ │ │ │ │ └─► 是 ──► storeLocked() │ │ │ └─► 新 key ──► dirty 为空 │ │ │ ├─► 是 ──► dirtyLocked() │ │ 设置 amended true │ │ │ └─► dirty[key] newEntry() │ └─► 解锁sync.Map的核心思想是空间换时间通过维护两个 mapread 和 dirty读优先走 read无锁写走 dirty有锁。sync.Map通过读写分离、延迟删除、按需提升等策略在特定场景下显著降低了锁竞争。但它不是银弹理解其设计原理才能在正确的场景使用它。欢迎各位同学关注我哦~在这个 AI 喧嚣的时代不忘初心戒骄戒躁认真沉淀

更多文章