基本用法
我们都知道go
的map
是并发不安全的,当几个goruotine
同时对一个map
进行读写操作时,就会出现并发写问题:
package main
func main() {
var wg sync.WaitGroup
wg.Add(2)
m := make(map[int]int)
go func() {
i := 0
for ; i < 1000; i++ {
m[i] = i
}
wg.Done()
}()
go func() {
i := 0
for ; i < 1000; i++ {
_ = m[i]
}
wg.Done()
}()
wg.Wait()
fmt.Println("run end\n")
}
该程序不断对 m变量进行赋值与取值,理论上只要在多核cpu
下,如果子goroutine
和主goroutine
同时在运行,就会出现问题。我们不妨用go
自带的-race
来检测下,可以运行 go run -race main.go
:
go run -race .\main.go
==================
WARNING: DATA RACE
Read at 0x00c00001e030 by goroutine 7:
runtime.mapaccess1_fast64()
C:/Go/src/runtime/map_fast64.go:12 +0x0
main.main.func2()
D:/wamp64/www/golang/src/main/main.go:23 +0x54
..................
通过检测,我们可以发现,存在data race
,即数据竞争问题。
遇到数据竞争问题,如何解决呢?
有人说这简单,加锁解决,加锁固然可以解决,那我们用加锁解决试试:
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.RWMutex
m := make(map[int]int)
wg.Add(2)
go func() {
i := 0
for ; i < 1000; i++ {
mu.Lock()
m[i] = i
mu.Unlock()
}
wg.Done()
}()
go func() {
i := 0
for ; i < 1000; i++ {
mu.RLock()
_ = m[i]
mu.RUnlock()
}
wg.Done()
}()
wg.Wait()
fmt.Println("run end\n")
}
加锁的确解决了问题,但是大家都知道加锁会消耗大量开销,在高并发场景下,锁的争用会造成系统性能的下降。
那么就用go
内置的方法sync.Map
,它可以解决并发问题。
sync.Map
与map
不同,不是以语言原生形态提供,而是在 sync
包下的特殊结构:
- 无须初始化,直接声明即可。
sync.Map
不能使用map
的方式进行取值和设置等操作,而是使用sync.Map
的方法进行调用,Store
表示存储,Load
表示获取,Delete
表示删除。- 使用
Range
配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range
参数中回调函数的返回值在需要继续迭代遍历时,返回true
,终止迭代遍历时,返回false
。
sync.Map
对外提供了以下常用方法:
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {...} //获取数据
func (m *Map) Store(key, value interface{}) {...} //储存数据
func (m *Map) Delete(key interface{}) {...} //删除数据
func (m *Map) Range(f func(key, value interface{}) bool) {...} //遍历数据
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {......}
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {......}
基本使用示例:
import (
"fmt"
"sync"
)
func main() {
//声明一个sync.Map变量m
m := sync.Map{}
//存数据
m.Store(1, 1)
m.Store(2, 2)
m.Store(3, 3)
//取数据
k1, ok := m.Load(1)
fmt.Println(k1, ok)
//遍历map
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
//删除map数据
m.Delete(1)
}
基础使用熟悉后,我们用 sync.map
解决上面的并发问题:
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
m := sync.Map{}
wg.Add(2)
go func() {
i := 0
for ; i < 1000; i++ {
m.Store(i, i)
}
wg.Done()
}()
go func() {
i := 0
for ; i < 1000; i++ {
_, _ = m.Load(i)
}
wg.Done()
}()
wg.Wait()
fmt.Println("run end\n")
}
sync.Map
确实可以解决并发map
问题,但是它在读多写少的情况下,比较适合,可以保证并发安全,同时又不需要锁的开销,在写多读少的情况下反而可能会更差,主要是因为它的设计。
所以在合适的地方使用合适的方案,是解决并发问题根本办法。总结起来:
- 并发量少的情况下可以使用锁
- 并发量大并且读多写少的情况可以使用
sync.Map
- 并发量大并伴有大量写操作,可以考虑 对内部
map
进行分片,降低锁粒度,从而达到最少的锁等待时间方案
结构体
sync.Map
的核心数据结构如下:
//go 1.20.3 path: src/sync/map.go
type any = interface{}
type Map struct {
mu Mutex
read atomic.Pointer[readOnly]
dirty map[any]*entry
misses int
}
结构体字段分析如下:
-
mu
互斥锁 ,当涉及到脏数据(dirty
)操作时候,需要使用这个锁; -
dirty
是一个非线程安全的原始map
,提供读写分离的写功能,用Mutex
锁进行保护。 -
read
一个atomic.Pointer
指针类型,其值指向是readOnly
类型, 并发是安全的,readOnly
类型对应的是一个只读map
,数据结构定义如下:type readOnly struct { m map[any]*entry amended bool } type entry struct { p atomic.Pointer[any] }
字段解释如下:
readOnly.m
是一个map
结构,结构跟sync.Map.dirty
一致,其键为一个任意数值,其值指向一个*interface{}
;readOnly.amended
为一个判断sync.Map.read
中的数据是否存在缺失,需要通过dirty map
兜底,值分两种情况:amended
为true
,表示sync.Map.dirty
中存在sync.Map.read
未包含的数据,需要再查询sync.Map.dirty
中是否包含该key
;amended
为false
,表示sync.Map.dirty
为空,需要重构sync.Map.dirty
数据;
entry.p
是一个interface{}
类型指针,可以为任何数据,其拥有三种状态:- 当
p == nil
时,说明这个键值对已被删除(软删除),read map
和dirty map
底层的map
结构仍存在key-entry
对,但在逻辑上该key-entry
对已经被删除,因此无法被用户查询到; - 当
p == expunged
时,说明这条entry
键值对已被删除(硬删除),sync.Map.dirty
中彻底没有这个key-entry
对; - 其他值则表示
p
指向一个正常的。
- 当
-
misses
每当从read
中读取失败,都会将misses
的计数值加1
,当加到一定阈值以后,需要将dirty
提升为read
,以期减少miss
的情形。
如下图表示sync.Map
的字段之间结构体的大致互相关系:
通过上述关系图可以看出 sync.Map.read.m
与sync.Map.dirty
对应的数据结构体是一致的,那它们之间是否有什么关系呢? sync.Map
为什么这么设计呢?
这个我们从源码中去寻找这些问题的答案。
源码分析
sync.Map.Load
sync.Map.Load
是负责查询和读取map
数据的函数, 该函数读取指定 key
返回 value
,直接开始扒源码:
//go 1.20.3 path: src/sync/map.go
func (m *Map) Load(key any) (value any, ok bool) {
// 读取只读的map,即sync.Map.read中的map
read := m.loadReadOnly()
// 从只读的map中读取key对应的value
e, ok := read.m[key]
if !ok && read.amended { // 如果没有找到值,并且sync.Map.dirty中有数据,则执行下面的逻辑
m.mu.Lock() // 加锁
read = m.loadReadOnly() // 重新读取只读的map
e, ok = read.m[key] // 从只读的map中读取key对应的value
// 如果没有找到值,并且sync.Map.dirty中有数据,则执行下面的逻辑
if !ok && read.amended {
// 从sync.Map.dirty中读取key对应的value
e, ok = m.dirty[key]
m.missLocked() // 统计miss次数
}
m.mu.Unlock() // 解锁
}
//如果还是没有找到值,则返回nil
if !ok {
return nil, false
}
// 返回找到的值
return e.load()
}
func (m *Map) loadReadOnly() readOnly {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly{}
}
func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&x.v)) }
func (e *entry) load() (value any, ok bool) {
p := e.p.Load()
// 如果p为nil或者p为expunged,则返回nil
if p == nil || p == expunged {
return nil, false
}
return *p, true
}
sync.Map.Load
整体流程:
- 首先进入
fast path
(快捷方式),直接在sync.Map.read
中找key
,如果找到了直接调用entry
的load
方法,取出其中的值; - 如果
sync.Map.read
中没有这个key
,则分情况处理:- 如果
amended
为fase
,说明sync.Map.dirty
为空(在missLocked
处理的时候,会将amended
设置为false
,此时sync.Map.dirty
会被清空),直接返回空和false
; - 如果
amended
为true
,说明sync.Map.dirty
中可能存在我们要找的key
,当然要先上锁,再尝试去sync.Map.dirty
中查找。在这之前,仍然有一个double check
的操作,再去sync.Map.read
查找一次这个key
。若还是没有在sync.Map.read
中找到,那么就从sync.Map.dirty
中找。不管sync.Map.dirty
中有没有找到,都要记一笔misses+1
,因为在sync.Map.dirty
被提升为sync.Map.read
之前,都会进入这条路径;
- 如果
再来看看sync.Map.Load
中调用关于misses
操作的函数missLocked
:
//go 1.20.3 path: src/sync/map.go
func (m *Map) missLocked() {
//miss次数加1
m.misses++
// 如果miss次数小于sync.Map.dirty中的数据量,则直接返回
if m.misses < len(m.dirty) {
return
}
// 如果miss次数大于等于sync.Map.dirty中的数据量,则执行下面的逻辑
m.read.Store(&readOnly{m: m.dirty}) // 将sync.Map.dirty中的数据赋值给sync.Map.read
m.dirty = nil // 清空sync.Map.dirty
m.misses = 0 // 将miss次数置为0
}
该代码主要作用是统计将 misses+1
,此时根据misses
值分两种情况处理:
- 如果
misses
值小于sync.Map.dirty
长度,则直接返回; - 如果
misses
大于或等于sync.Map.dirty
的长度,则将sync.Map.dirty
中的数据赋值给sync.Map.read
,然后清空的sync.Map.dirty
, 同时misses
设置为0。
这就是说 sync.Map.read
好比整个sync.Map
的一个“高速缓存”,当goroutine
从sync.Map
中读数据时,sync.Map
会首先查看read
这个缓存层是否有用户需要的数据(key
是否命中),如果有(key
命中),则通过原子操作将数据读取并返回,这是sync.Map
推荐的快路径(fast path
),也是sync.Map
的读性能极高的原因。
下面用一张图来完整的归纳总结下sync.Map.Load
函数流程:
sync.Map.Store
sync.Map.Store
函数主要负责更新数据, 更新指定key
的value
,假如该key
已经存在则更新 value
, 不存在则插入新数据。
sync.Map.Store
函数源码如下:
//go 1.20.3 path: src/sync/map.go
func (m *Map) Store(key, value any) {
//调用swap方法
_, _ = m.Swap(key, value)
}
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
// 返回值 previous表示替换前的值,loaded表示key是否存在
// 读取只读的map,即sync.Map.read中的map
read := m.loadReadOnly()
if e, ok := read.m[key]; ok { //read.m中查询key是否存在,如果存在执行该逻辑
// 执行替换操作,将值替换为value
if v, ok := e.trySwap(&value); ok {
//如果v为nil,则表示key之前已经被删除了,直接返回nil,false
if v == nil {
return nil, false
}
//如果v不为nil,则表示key存在,返回替换前的值
return *v, true
}
}
//运行到此处,表示key在read.m中不存在,执行下面的逻辑
m.mu.Lock() // 加锁
read = m.loadReadOnly() // 重新读取只读的map,即sync.Map.read中的map
if e, ok := read.m[key]; ok { //从read.m查询key是否存在,如果存在,则执行下面的逻辑
// 如果read值域中entry已经删除被标记为expunged,则表明dirty没有该key,则可添加到dirty中,并更新entry
if e.unexpungeLocked() {
m.dirty[key] = e
}
//调用swapLocked方法执行替换操作
if v := e.swapLocked(&value); v != nil {
loaded = true //标记loaded为true,表示key存在
previous = *v //将前值赋予previous
}
} else if e, ok := m.dirty[key]; ok { //如果read值域中entry不存在,则从dirty中查询key是否存在,如果存在,则执行下面的逻辑
//调用swapLocked方法执行替换操作
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else { //如果read值域中entry不存在,dirty中也不存在,则执行下面的逻辑
//如果read.amended为false,表示中的map数据缺失
if !read.amended {
//调用dirtyLocked方法,将read.m中的数据复制到sync.Map.dirty中
m.dirtyLocked()
//将read.amended置为true
m.read.Store(&readOnly{m: read.m, amended: true})
}
//将key和value添加到sync.Map.dirty中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock() // 解锁
return previous, loaded //返回
}
sync.Map.Store
函数中还有一些辅助函数,在此处给出:
//go 1.20.3 path: src/sync/map.go
type any = interface{}
var expunged = new(any)
// trySwap 尝试将entry的值替换为i
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
// unexpungeLocked 将entry的值从expunged状态恢复到正常状态
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return e.p.CompareAndSwap(expunged, nil)
}
// swapLocked 将entry的值替换为i
func (e *entry) swapLocked(i *any) *any {
return e.p.Swap(i)
}
// dirtyLocked 将read.m中的数据复制到sync.Map.dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
sync.Map.Store
函数整体流程:
-
如果在
read
里能够找到待存储的key
,并且对应的entry
的p
值不为expunged
,也就是没被删除时,直接更新对应的entry
即可; -
如果
read
中没有这个key
,则先加锁,再double check
一下,再检查一次read
中是否存在这个key
,这也是lock-free
编程里的常见套路。如果read
中存在该key
,但p == expunged
,说明m.dirty != nil
并且m.dirty
中不存在该key
值 此时:- 将
p
的状态由expunged
更改为nil
,至于为啥要设置为nil
,最后总结中会讲到; dirty map
插入key
。然后直接更新对应的value
;
- 将
-
如果
read
中没有此key
,那就查看dirty
中是否有此key
,如果有,则直接更新对应的value
,这时read
中还是没有此key
; -
最后一步,如果
read
和dirty
中都不存在该key
,则:- 如果
dirty
为空,则需要创建dirty
,并从read
中拷贝未被删除的(即值不为nil
或者expunge
的)元素; - 更新
amended
字段值为true
,标识dirty map
中存在read map
中没有的key
(即后面加入dirty map
的新的key-entry
); - 将
k-v
写入dirty map
中,read.m
不变; - 最后更新此
key
对应的value
;
- 如果
-
最后,释放锁,结束赋值过程。
sync.Map.Store
函数整体流程图:
sync.Map.Delete
sync.Map.Delete
函数负责删除Map
中的数据,具体代码如下:
//go 1.20.3 path: src/sync/map.go
func (m *Map) Delete(key any) {
//调用LoadAndDelete方法,如果返回的loaded为true,则说明删除成功
m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
//获取只读的map,即sync.Map.read
read := m.loadReadOnly()
//从read.m中获取key对应的entry
e, ok := read.m[key]
if !ok && read.amended { //如果read.m中不存在key对应的entry,并且read.amended为true
m.mu.Lock() //加锁
//double check.再次获取只读的map,即sync.Map.read
read = m.loadReadOnly()
//再次从read.m中获取key对应的entry
e, ok = read.m[key]
if !ok && read.amended { //如果read.m中不存在key对应的entry,并且read.amended为true
//从sync.Map.dirty中获取key对应的entry
e, ok = m.dirty[key]
//从sync.Map.dirty中删除key对应的entry
delete(m.dirty, key)
//更新miss字段,如果miss字段大于等于dirty的长度的一半,则将dirty中的数据复制到read.m中
m.missLocked()
}
m.mu.Unlock() //解锁
}
//如果read.m中存在key对应的entry,则直接删除
if ok {
return e.delete()
}
// 返回
return nil, false
}
相关辅助函数:
//删除entry
func (e *entry) delete() (value any, ok bool) {
for {
p := e.p.Load() //获取entry的值
//如果entry的值为nil或者expunged,则直接返回
if p == nil || p == expunged {
return nil, false
}
//如果entry的值不为nil或者expunged,则将entry的值替换为nil,并返回entry的值
if e.p.CompareAndSwap(p, nil) {
return *p, true
}
}
}
sync.Map.Delete
函数基本流程:
- 先从
read
里查是否有这个key
,如果有则执行entry.delete
方法,将p
置为nil
,这样read
和dirty
都能看到这个变化; - 如果没在
read
中找到这个key
,并且dirty
不为空,那么就要操作dirty
了,操作之前,还是要先上锁。然后进行double check
,如果仍然没有在read
里找到此key
,则从dirty
中删掉这个key
。但不是真正地从dirty
中删除,而是更新entry
的状态
sync.Map.Delete
整体流程图:
sync.Map.Range
sync.Map.Range
的参数是一个函数:
f func(key, value interface{}) bool
提供了O(N)
的方式遍历sync.Map
,用户传入遍历key-value
的动作函数,如果函数返回false
,则遍历终止;即使在遍历过程中发生key
的并发读写操作,每个key
也仅会被最多遍历一次。因为 dirty
可能包含read
中不存在的key
的状态,当遍历操作发生时,如果dirty
与read
存储的有效key
的状态不一致,将dirty
提升为read
。
sync.Map.Range
的源码如下:
func (m *Map) Range(f func(key, value any) bool) {
//获取只读的map,即sync.Map.read
read := m.loadReadOnly()
if read.amended { //如果read.amended为true,意味着sync.Map.read中的数据数据过期,需要从sync.Map.dirty中获取数据
m.mu.Lock() //加锁
//double check, 再次获取只读的map,即sync.Map.read
read = m.loadReadOnly()
if read.amended { //如果read.amended为true,意味着sync.Map.read中的数据数据过期,需要从sync.Map.dirty中获取数据
//从sync.Map.dirty中获取数据
read = readOnly{m: m.dirty}
//更新sync.Map.read
m.read.Store(&read)
//将sync.Map.dirty置为nil
m.dirty = nil
//将misses置为0
m.misses = 0
}
m.mu.Unlock() //解锁
}
//遍历sync.Map.read中的数据
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) { //调用f
break
}
}
}
上述代码流程比较简单,如下:
-
read
包含所有有效元素时,直接遍历read
中存储的值; -
dirty
含有read
中不存在的元素时,将dirty
提升为read
,再遍历新read
中存储的;
sync.Map.Range
流程图如下:
高并发安全Map的解决方案
orcaman/concurrent-map
的适用场景是:反复插入与读取新值,其实现思路是:对go
原生map
进行分片加锁,降低锁粒度,从而达到最少的锁等待时间(锁冲突)。
concurrent-map
源码地址:https://github.com/orcaman/concurrent-map
部分实现源码:
// SHARD_COUNT 分片大小
var SHARD_COUNT = 32
// string:Anything 类型的“线程”安全映射
// // 为了避免锁瓶颈,这个map被嵌入了几个 (SHARD_COUNT) map shards。
type ConcurrentMap[V any] []*ConcurrentMapShared[V]
// 任何映射的“线程”安全字符串
type ConcurrentMapShared[V any] struct {
items map[string]V
sync.RWMutex // 读写互斥锁,保护对内部映射的访问
}
// 创建一个新的并发映射map.
func New[V any]() ConcurrentMap[V] {
m := make(ConcurrentMap[V], SHARD_COUNT)
for i := 0; i < SHARD_COUNT; i++ {
m[i] = &ConcurrentMapShared[V]{items: make(map[string]V)}
}
return m
}
函数介绍:
-
Get 方法
// 先hash拿到key对应的分区号,然后加锁,读取值,最后释放锁和返回 func (m ConcurrentMap[V]) Get(key string) (V, bool) { // Get shard shard := m.GetShard(key) shard.RLock() // Get item from shard. val, ok := shard.items[key] shard.RUnlock() return val, ok }
-
Set 方法
// 先hash拿到key对应的分区号,然后加锁,设置新值,最后释放锁 func (m ConcurrentMap[V]) Set(key string, value V) { // Get map shard. shard := m.GetShard(key) shard.Lock() shard.items[key] = value shard.Unlock() }
-
Remove 方法
// 先hash拿到key对应的分区号,然后加锁,删除key,最后释放锁 func (m ConcurrentMap[V]) Remove(key string) { // Try to get shard. shard := m.GetShard(key) shard.Lock() delete(shard.items, key) shard.Unlock() }
-
Count 方法
// 分别拿到每个分片map中的元素数量,然后汇总后返回 func (m ConcurrentMap[V]) Count() int { count := 0 for i := 0; i < SHARD_COUNT; i++ { shard := m[i] shard.RLock() count += len(shard.items) shard.RUnlock() } return count }
-
Upsert 方法
// 先hash拿到key对应的分区号,然后加锁,如果key存在就更新其value,否则插入新的k-v,释放锁并返回 func (m ConcurrentMap[V]) Upsert(key string, value V, cb UpsertCb[V]) (res V) { shard := m.GetShard(key) shard.Lock() v, ok := shard.items[key] res = cb(ok, v, value) shard.items[key] = res shard.Unlock() return res }
总结
entry.p 状态
在结构体内容部分介绍过,entry.p
是一个interface{}
类型指针,其拥有三种状态:nil
、expunged
、其他正常值。
那nil
、expunged
有啥区别呢?
其实 nil
、expunged
都用来表示key
值已经被删除了,但区别的是:
nil
值代表软删除态:read map
和dirty map
在物理上仍保有该key-entry
对,因此倘若此时需要对该entry
执行写操作,可以直接CAS
操作;expunged
值代表硬删除态:dirty map
中已经没有该key-entry
对,倘若执行写操作,必须加锁(dirty map
必须含有全量key-entry
对数据);
设计 expunged
和 nil
两种状态的原因,就是为了优化在 dirtyLocked
前,针对同一个 key
先删后写的场景. 通过 expunged
态额外标识出 dirty map
中是否仍具有指向该 entry
的能力,这样能够实现对一部分 nil
态 key-entry
对的解放,能够基于 CAS
完成这部分内容写入操作而无需加锁。
下列图示演示了 nil
态软删除的数据的基于CAS
恢复:
dirty map 和 read map
前面章节已经对这这2
个map
进行了介绍,在源码分析中也讲到了,为了更清晰的说明这2
个map
的作用以及相互关系,这边重点再分析分析。
sync.Map
由两个重要的 map
构成:
read map
:访问时全程无锁,其内容为dirty map
的子集;dirty map
:是兜底的读写map
,访问时需要加锁;
sync.Map
本质上采取了一种以空间换时间 + 动态调整策略的设计思路 ,希望在读、更新、删频次较高时,更多地采用 CAS
的方式无锁化地完成操作;在写操作频次较高时,则直接了当地采用加锁操作完成。
总体思想,希望能多用 read map
,少用 dirty map
,因为操作前者无锁,后者需要加锁。如下图所示:
来看看什么情况下会从dirty map
复制数据给read map
?
通过源码阅读,我们在函数 missLocked
中找到了相关操作:
//go 1.20.3 path: src/sync/map.go
func (m *Map) missLocked() {
//miss次数加1
m.misses++
// 如果miss次数小于sync.Map.dirty中的数据量,则直接返回
if m.misses < len(m.dirty) {
return
}
// 如果miss次数大于等于sync.Map.dirty中的数据量,则执行下面的逻辑
m.read.Store(&readOnly{m: m.dirty}) // 将sync.Map.dirty中的数据赋值给sync.Map.read
m.dirty = nil // 清空sync.Map.dirty
m.misses = 0 // 将miss次数置为0
}
从代码可以看出,当misses
次数(read
状态命中失败的次数)大于等于dirty map
的数据量时,此时将dirty
提升为read
,老 dirty
作为新 read
,并将新的dirty map
设置为nil
,将 misses
计时器重置为0
。(注意此时m.amended
默认设置为false
)
这么做的是提升下次只读状态read
查询的命中率,避免加锁操作。
那又是什么情况下会从read map
数据复写给dirty map
?
通过源码,可以得出当dirty
暂时为空(此时,m.amended
应该为false
)且接下来一次写操作访问 read
时 miss
,需要进行dirtyLocked
操作,在dirtyLocked
操作过程中会将read map
数据复写给dirty map
,来看下dirtyLocked
函数代码:
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
在 dirtyLocked
流程中,需要对 read
内的元素进行状态更新,因此需要遍历。dirtyLocked
遍历中,会将 read
中未被删除的元素(非 nil
非 expunged
)拷贝到 dirty
中;会将 read
中所有此前被删的元素统一置为 expunged
态。
如下图:
至此,内容就这些吧。
参考资料:
假装懂编程 https://baijiahao.baidu.com/s?id=1705075387433772703&wfr=spider&for=pc
付少华 https://blog.csdn.net/weixin_44014995/article/details/109260782
咖啡色的羊驼 https://blog.csdn.net/u011957758/article/details/96633984
背着电脑去搬砖 https://blog.csdn.net/xzw12138/article/details/109505767
[小徐先生的编程世界](javascript:void(0)😉 https://mp.weixin.qq.com/s/nMuCMA8ONnhs1lsTVMcNgA