王者名望曜幻光灵剑若干很多若干良多若干很多若干良多若干很多若干钱 王者名望曜幻光灵剑价值引见
1627 2026-07-28 23:49
map 的读写两种如今在业界独霸的最多的并发支撑的编制分袂是 :
有了选择,读写老是读写有选择艰苦症的,这两种幻想下场若何选 ,读写谁的读写功用愈加的好 ?我有一个同伙说 标准库 sync.Map 功用菜的很 ,不要用。读写我幻想下场听谁的读写...
往日煎鱼就带你揭秘 Go sync.map ,我们先会体味了然甚么场景下 ,读写Go map 的读写多种圭表类型若何用 ,谁的读写功用最好!
接着屈就各 map 功用分化的读写下场,针对性的读写对 sync.map 举办源码解剖 ,体味 WHY 。读写
一同快活地最早吸鱼之路 。读写
在 Go 官方文档中了了指出 Map 圭表类型的一些建议 :

同时 Map 圭表类型,还针对以终局景举办了功用优化:
这两种气候与 Go map 搭配孤单的 Mutex 或 RWMutex 对角力筹算,独霸 Map 圭表类型可以除夜除夜促进锁的掠夺 。
听官方文档引见了一堆益处后,他并没有讲到偏向偏向,所说的功用优化后的优势又可否真实可托。我们一同来验证一下。
起首我们定义根本的数据筹划:
// 代表互斥锁type FooMap struct { sync.Mutex data map[int]int}// 代表读写锁type BarRwMap struct { sync.RWMutex data map[int]int}var fooMap *FooMapvar barRwMap *BarRwMapvar syncMap *sync.Map// 初始化根本数据筹划func init() { fooMap = &FooMap{ data: make(map[int]int, 100)} barRwMap = &BarRwMap{ data: make(map[int]int, 100)} syncMap = &sync.Map{ }}在配套编制上,罕有的增删改查行动我们都编写了照顾的编制 。用于后续的压测(只展示部分代码) :
func builtinRwMapStore(k, v int) { barRwMap.Lock() defer barRwMap.Unlock() barRwMap.data[k] = v}func builtinRwMapLookup(k int) int { barRwMap.RLock() defer barRwMap.RUnlock() if v, ok := barRwMap.data[k]; !ok { return -1 } else { return v }}func builtinRwMapDelete(k int) { barRwMap.Lock() defer barRwMap.Unlock() if _, ok := barRwMap.data[k]; !ok { return } else { delete(barRwMap.data, k) }}此外的圭表类型编制根本近似,思虑几回篇幅问题是以就不在此展示了。
压测编制根本代码以下 :
func BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) { b.RunParallel(func(pb *testing.PB) { r := rand.New(rand.NewSource(time.Now().Unix())) for pb.Next() { k := r.Intn(100000000) builtinRwMapDelete(k) } })}这块次要就是增删改查的代码和压测编制的预备 ,压测代码直接复用的是除夜白除夜佬的 go19-examples/benchmark-for-map 项目。
也可独霸 Go 官方供给的 map\_bench\_test.go ,有欢欣乐乐喜悦爱好的小火伴可以本身拉上往运转试一下。
| 名 | 含义 | 压测下场 |
|---|---|---|
| BenchmarkBuiltinMapStoreParalell-4 | map+mutex 写进元素 | 237.1 ns/op |
| BenchmarkSyncMapStoreParalell-4 | sync.map 写进元素 | 509.3 ns/op |
| BenchmarkBuiltinRwMapStoreParalell-4 | map+rwmutex 写进元素 | 207.8 ns/op |
集团的排序(从慢到快)为:SyncMapStore < MapStore < RwMapStore。
| 编制名 | 含义 | 压测下场 |
|---|---|---|
| BenchmarkBuiltinMapLookupParalell-4 | map+mutex 查找元素 | 166.7 ns/op |
| BenchmarkBuiltinRwMapLookupParalell-4 | map+rwmutex 查找元素 | 60.49 ns/op |
| BenchmarkSyncMapLookupParalell-4 | sync.map 查找元素 | 53.39 ns/op |
在查找元素上,最慢的是原生 map+互斥锁,其次是原生 map+读写锁。最快的是 sync.map 圭表类型 。
集团的排序为:MapLookup < RwMapLookup < SyncMapLookup 。
| 编制名 | 含义 | 压测下场 |
|---|---|---|
| BenchmarkBuiltinMapDeleteParalell-4 | map+mutex 删除元素 | 168.3 ns/op |
| BenchmarkBuiltinRwMapDeleteParalell-4 | map+rwmutex 删除元素 | 188.5 ns/op |
| BenchmarkSyncMapDeleteParalell-4 | sync.map 删除元素 | 41.54 ns/op |
在删除元素上 ,最慢的是原生 map+读写锁,其次是原生 map+互斥锁 ,最快的是 sync.map 圭表类型。
集团的排序为 :RwMapDelete < MapDelete < SyncMapDelete 。
屈就上述的压测下场,我们可以得出 sync.Map 圭表类型 :
是以在理论的营业场景中。假定是读多写少的场景,会更建议独霸 sync.Map 圭表类型 。
但假定是那种写多的场景,比如多 goroutine 批量的轮回写进 ,那就建议另辟路途了,功用不忍直视(无功用请求另当别论) 。
了然若何测试,测试的下场后。我们需求进一步深挖,知其所以然。
为甚么 sync.Map 圭表类型的测试下场这么的 “偏科”,为甚么读独霸功用这么高 ,写独霸功用低的恐怖 ,他是若何筹划的 ?
sync.Map 圭表类型的底层数据筹划以下:
type Map struct { mu Mutex read atomic.Value // readOnly dirty map[inte***ce{ }]*entry misses int}// Map.read 属性理论存储的是 readOnly。type readOnly struct { m map[inte***ce{ }]*entry amended bool}在 read 和 dirty 中,都有触及到的筹划体 :
type entry struct { p unsafe.Pointer // *inte***ce{ }}其包含一个指针 p, 用于指向用户存储的元素(key)所指向的 value 值。
在此建议你必须弄懂 read 、dirty、entry,再往下看,食用终局会更佳,后续会旋绕着这几个定见流转。
划重点 ,Map 圭表类型本质上是有两个 “map”。一个叫 read 、一个叫 dirty ,长的也差不多 :

sync.Map 的 2 个 map
当我们从 sync.Map 圭表类型中读取数据时,其会先搜检 read 中可否包含所需的元素:
sync.Map 的读独霸功用如斯之高的启事 ,就在于存在 read 这一奇妙的筹划 ,其作为一个缓存层,供给了快路途(fast path)的查找。
同时其连络 amended 属性,配套措置了每次读取都触及锁的问题,完成了读这一个独霸处景的高功用。
我们直接存眷 sync.Map 圭表类型的 Store 编制 ,该编制的感染是新增或更新一个元素。
源码以下:
func (m *Map) Store(key, value inte***ce{ }) { read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return } ...}调用 Load 编制搜检 m.read 中可否存在这个元素 。若存在 ,且没有被标识表记标帜为删除外形,则考验考验存储。
若该元素不存在或已被标识表记标帜为删除外形 ,则延续走到上面流程 :
func (m *Map) Store(key, value inte***ce{ }) { ... m.mu.Lock() read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { if e.unexpungeLocked() { m.dirty[key] = e } e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { e.storeLocked(&value) } else { if !read.amended { m.dirtyLocked() m.read.Store(readOnly{ m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock()}因为已走到了 dirty 的流程 ,是以开首就直接调用了 Lock 编制上互斥锁,担保数据安然,也是凸显功用变差的第一幕 。
其分为以下三个措置分支:
我们理一理 ,写进过程的集团流程就是 :
回到最初的话题,为甚么他写进功用差那么多。究其启事:
可得知 sync.Map 圭表类型不契合写多的场景 ,读多写少是斗劲好的。
如有除夜数据量的场景,则需求思虑 read 复制数据时的有时功用股栗可否可以领受。
这时辰辰大年夜大年夜约有小火伴在想了。写进过程 ,幻想上和删除不会差太远。若何 sync.Map 圭表类型的删除的功用似乎还行,这里面有甚么猫腻?
源码以下:
func (m *Map) LoadAndDelete(key inte***ce{ }) (value inte***ce{ }, loaded bool) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] ... if ok { return e.delete() }}删除是标准的停止 ,还是先到 read 搜检该元素可否存在。
若存在 ,则调用 delete 标识表记标帜为 expunged(删除外形) ,特别很是高效 。可以了了在 read 中的元素,被删除 ,功用黑色常好的。
若不存在 ,也就是走到 dirty 流程中 :
func (m *Map) LoadAndDelete(key inte***ce{ }) (value inte***ce{ }, loaded bool) { ... if !ok && read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] delete(m.dirty, key) m.missLocked() } m.mu.Unlock() } ... return nil, false}若 read 中不存在该元素 ,dirty 不为空 ,read 与 dirty 不合等(独霸 amended 分辨),则注解要独霸 dirty,上互斥锁。
再几回举办两重搜检 ,若 read 还是不存在该元素 。则调用 delete 编制从 dirty 中标识表记标帜该元素的删除 。
需求寄看 ,展示频率较高的 delete 编制:
func (e *entry) delete() (value inte***ce{ }, ok bool) { for { p := atomic.LoadPointer(&e.p) if p == nil || p == expunged { return nil, false } if atomic.CompareAndSwapPointer(&e.p, p, nil) { return *(*inte***ce{ })(p), true } }}该编制都是将 entry.p 置为 nil,并且标识表记标帜为 expunged(删除外形),而不是真真正正的删除。
注 :不要误用 sync.Map ,前段时分从字节除夜佬分享的案例来看,他们将一个邻接作为 key 放了进往 ,是以和这个邻接相干的,比如 :buffer 的内存就永远没法释放了...
总结 :
针对 sync.Map 的功用不合,举办了深切的源码分化 ,体味到了其面前快、慢的启事,完成了知其然知其所以然。
经常看到并发读写 map 招致致命偏向,理论上是令人忧心 。大年夜师感应感染假定本文不错,迎接分享给更多的 Go 欢欣乐乐喜悦爱好者 :)
到此这篇关于Go 并发读写 sync.map 具体的文章就引见到这了,更多相干Go言语 并发读写 sync.map 内容请搜刮完竣下载之前的文章或延续不雅不雅不雅不雅鉴赏上面的相干文章希看大年夜师往后多多支撑完竣下载 !