更方便的在微信公众号阅读文章可以关注公众号:海生的go花园
一、什么是 sync.Map
sync.Map,是一种可以像Go语言中的Map那样以Key/Value格式将值存储在内存中。
sync通用Mutex,可以在多个goroutine并发执行上也可以安全使用。
我们可以把它当做和gocache
或者Redis一样的缓存来使用。
适用的场景为 写少,读多
的地方。
我们在命令行中输入:go doc sync.map
基于go1.20版本,可以使用的功能如下。
type Map struct {}
// 常用
func (m *Map) Store(key, value any)
func (m *Map) Delete(key any)
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) Range(f func(key, value any) bool)
// 其他
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) Swap(key, value any) (previous any, loaded bool)
func (m *Map) CompareAndDelete(key, old any) (deleted bool)
func (m *Map) CompareAndSwap(key, old, new any) bool
现在我们来一边使用,一边学习。
1.1 Store存储/Range遍历
我们顺序的往sync.map存储多个值,然后遍历sync.map,输出刚才的key/value
func TestStoreAndRange(t *testing.T) {
// 初始化
m := sync.Map{}
// 使用Store,增加 key/value
m.Store("Key 1", "Value 1")
m.Store("Key 2", "Value 2")
m.Store("Key 3", "Value 3")
m.Store("Key 4", "Value 4")
m.Store("Key 5", "Value 5")
// 遍历sync.map 来获取刚才存储的 key/value
m.Range(func(key, value any) bool {
fmt.Printf("Key: %v(Type: %T) -> Value: %v(Type: %T)\n", key, key, value, value)
return true
})
}
输出的结果,每一次都不同:
Key: Key 3 -> Value: Value 3
Key: Key 4 -> Value: Value 4
Key: Key 5 -> Value: Value 5
Key: Key 1 -> Value: Value 1
Key: Key 2 -> Value: Value 2
遍历输出的特征为:无序
因为sync.map这个结构体,存储的时候还是go map。
1.2 Delete 删除
我们在上面的代码基础上,删除一些key,然后再遍历出整个sync.map。
func TestDelete(t *testing.T) {
// 初始化
m := sync.Map{}
// 使用Store,增加 key/value
m.Store("Key 1", "Value 1")
m.Store("Key 2", "Value 2")
m.Store("Key 3", "Value 3")
m.Store("Key 4", "Value 4")
m.Store("Key 5", "Value 5")
// 遍历sync.map 来获取刚才存储的 key/value
m.Range(func(key, value any) bool {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
return true
})
// 删除
m.Delete("Key 2")
m.Delete("Key 3")
m.Delete("Key5")
m.Delete("Key 10")
// 遍历sync.map 来获取刚才存储的 key/value
fmt.Printf("删除后数据:\n")
m.Range(func(key, value any) bool {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
return true
})
}
1.3 Load 读取数据
我们读取数据有三种 load 读取数据,根据我们的需要来使用。
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
我们还是准备才开的数据:
// 初始化
m := sync.Map{}
// 使用Store,增加 key/value
m.Store("Key 1", "Value 1")
m.Store("Key 2", "Value 2")
m.Store("Key 3", "Value 3")
m.Store("Key 4", "Value 4")
m.Store("Key 5", "Value 5")
// 遍历sync.map 来获取刚才存储的 key/value
m.Range(func(key, value any) bool {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
return true
})
1.3.1 使用load()方法:
func (m *Map) Load(key any) (value any, ok bool)
Load方法 返回 map中key 存储的值,或者返回nil
ok返回值 表示 这个值是否存在map中。
t.Log(m.Load("Key 1")) // key 存在
t.Log(m.Load("Key 11")) // key 不存在
输出:
Value 1 true
<nil> false
1.3.2 使用LoadOrStore()方法:
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
LoadOrStore 返回指定key现在存在的值
loaded返回值为 如果是load读取返回true,如果是store返回false
t.Log(m.LoadOrStore("Key 1", "New Value 1")) // key 存在
t.Log(m.LoadOrStore("Key 2", nil)) // key 存在
t.Log(m.LoadOrStore("Key 11", "Value 11")) // key 不存在
t.Log(m.LoadOrStore("Key 12", nil)) // key 不存在
输出:
Value 1 true
Value 2 true
Value 11 false
<nil> false
1.3.3 使用LoadAndDelete()方法:
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
LoadAndDelete 删除指定的key,返回先前的值。
loaded 返回值,是key是否存在。
t.Log(m.LoadAndDelete("Key 1")) // key 存在
t.Log(m.LoadAndDelete("Key 11")) // key 不存在
输出:
Value 1 true
<nil> false
在这里可以休息一下
留白互动时间:
在这里可以休息一下,消化一下上面的使用。
1.4 多个goroutine并发
在多个goroutine并发的场景下,可以安全使用。
func TestGoroutineSyncMap(t *testing.T) {
m := sync.Map{}
for i := 0; i < 1000; i++ {
go func(i int) {
m.Store(i, i)
}(i)
}
<-time.After(1000 * time.Millisecond)
sum := 0
m.Range(func(key interface{}, value interface{}) bool {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
sum++
return true
})
t.Logf("m的大小为 %d", sum)
}
我们可以在并发的情况下查看一下 go map的使用
func TestGoroutineMap(t *testing.T) {
m := make(map[int]int, 0)
for i := 0; i < 1000; i++ {
go func(i int) {
m[i] = i
}(i)
}
<-time.After(1000 * time.Millisecond)
for key, value := range m {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
}
t.Logf("m的大小为 %d", len(m))
}
输出报错:
fatal error: concurrent map writes
goroutine 404 [running]:
command-line-arguments.TestGoroutineMap.func1(0x181)
/Users/staff/study/sync-map/goroutine_test.go:35 +0x3
因为go map 不是并发安全的,我们如要在并发的情况下使用,需要结合sync.Mutex互斥锁
func TestGoroutineMapMutex(t *testing.T) {
m := make(map[int]int, 0)
var mu sync.Mutex
for i := 0; i < 1000; i++ {
go func(i int) {
mu.Lock()
m[i] = i
mu.Unlock()
}(i)
}
<-time.After(1000 * time.Millisecond)
for key, value := range m {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
}
t.Logf("m的大小为 %d", len(m))
}
而sync map,其实本质上,就是 go map + sync.Mutex 的这个版本的,升格版本。
这个版本的代码,都读写同一个 map,sync.map 进行了读写分离,通过两个map,来提高效率。
对比一下:
两种方案对比 | go map + sync.Mutex | sync.Map |
---|---|---|
占用内存map | 1个map | 2个map,2倍的内存占用 |
写锁 | √ | √ |
读锁 | √ |
sync.Map的方案主要是对锁的优化,在只读的情况下,无锁。
通过增加 一个只读 read map 的方式来实现。
用增加一倍的内存,来实现,读效率的提升,适用于写一次,读n次的情况。
1.5 go map + sync.Mutex方案
在go源码:src/sync/map_reference_test.go文件里,官方实现了和sync.map同样的接口。
// RWMutexMap is an implementation of mapInterface using a sync.RWMutex.
type RWMutexMap struct {
mu sync.RWMutex
dirty map[any]any
}
func (m *RWMutexMap) Load(key any) (value any, ok bool) {
m.mu.RLock()
value, ok = m.dirty[key]
m.mu.RUnlock()
return
}
func (m *RWMutexMap) Store(key, value any) {
m.mu.Lock()
if m.dirty == nil {
m.dirty = make(map[any]any)
}
m.dirty[key] = value
m.mu.Unlock()
}
....这里我截取一部分,详细的可以看源码。
1.6 和 go map + sync.Mutex方案对比
在go源码:src/sync/map_bench_test.go文件里,官方有benchmark基准测试。我们可以看一下结果
BenchmarkLoadMostlyHits/*sync_test.RWMutexMap
BenchmarkLoadMostlyHits/*sync_test.RWMutexMap-8 15851244 80.08 ns/op
BenchmarkLoadMostlyHits/*sync.Map
BenchmarkLoadMostlyHits/*sync.Map-8 210334969 6.167 ns/op
sync.Map比go map 加锁,速度快10倍。
在这里可以休息一下
留白互动时间:
在这里可以休息一下,消化一下上面的使用。
二、sync.map实现原理
2.1 读写分离
sync.Map的架构是实时读写分离
,如下图。
dirty里面是全量数据
,read里面的数据是dirty某个节点,同步过去的上个时间段的全量数据
因为读写分离,我们写都是到 dirty 里面,而读在 read里面。那么才是开始,是下面的样子。 dirty 有值,read位空。
所有的 读,写都在 dirty。相当于只有一个 map。
这个就是我们 使用 go map + sync.mutex 互斥锁 架构。
2.2 什么时候把dirty数据复制到read中?
从上面的图,我们可以看出来,如果要提高效率,就是看read的击中率。
如果更多的数据,击中read,就不用走到dirty中。
我们在read中的操作是没有锁的,dirty的操作,都需要加锁。
互动环节:
在这里,有很多种场景的应用,这里大家可以发言环节,说一下自己的方案?
2.3 sync.Map把dirty同步到read的时机。
sync.Map{}这个结构体,在设计的时候,增加了一个字段misses,来记录未命中read,击中dirty的次数。当次数达到dirty的长度时候,会同步。
map结构体源码如下:
type Map struct {
mu Mutex // sync.Mutex 互斥锁
read atomic.Pointer[readOnly] //read数据库
dirty map[any]*entry //dirty 数据库
misses int // 未命中read,击中dirty的次数。
}
dirty同步到read的源码如下:
// missLocked 当m.misses未击中 read map,走到 dirty map 中的 次数小于 len(m.dirty),
// 会一直走到这里,m.misses增加1次
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
// 当 m.misses 达到 len(m.dirty)次数。
// 把 dirty map 增加到 read map 中。(此时 read map中的m值为原来的 read + dirty; amended 重置为false )
// 然后把 dirty map置空,m.misses归0,重新开始。
m.read.Store(&readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
同步后,读写分离的状况如下:
2.4 思考:此时如果一个 store写入操作,我们应该怎么做?
在这里可以休息一下
留白互动时间:
在这里可以休息一下,消化一下上面的使用。
2.5 sync.Map当dirty刚同步完read为空的时候,store写入操作
我们可以看源码Store方法。
// Store sets the value for a key.
// Store 存储指定key的值
func (m *Map) Store(key, value any) {
_, _ = m.Swap(key, value)
}
具体到当dirty刚同步到read为空的时候,这段代码为:
// dirtyLocked 初始化 dirty map 初始值 为 read map
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
// 初始化 dirty map ,把 read map 里面的未删除值,全部同步到 dirty map中
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
}
}
}
主要的作用就是,把read里面的有效数据 同步到dirty里面。这样保持dirty要么为nil无数据,有数据的时候,就是 全量数据
。
此时我们回到读写分离的最原始的那张图。
三、sync.map源码阅读
3.1 map 和 readOnly ,entry 结构体
type Map struct {
mu Mutex // sync.Mutex 互斥锁
read atomic.Pointer[readOnly] // 存储read数据的readOnly指针类型
dirty map[any]*entry // 全量数据 dirty map
misses int //确认dirty同步到read时机用的,记录未命中read,击中dirty的次数
}
sync.map 主要的思想,读写分离。所以这里有两个map,
一个read,为了提高效率这里用了readOnly结构体原子化操作。
mu,主要是为了并发安全。
misses字段用来确认dirty同步到read时机用的,记录未命中read,击中dirty的次数
// readOnly is an immutable struct stored atomically in the Map.read field.
// readOnly 是一个 不可变的结构体,原子化存储在 sync.Map的read 字段中。
type readOnly struct {
m map[any]*entry // 其中map[key],key为泛型,任意类型。
amended bool // true if the dirty map contains some key not in m.
// amended (简单说,就是记录一种状态的,标记dirty和read有无差异)
// 1 :有差异,返回true。
// 2 :没有差异。返回false
}
amended 字段来标记 dirty 和 read 有无差异。我们可以总结一下几种情况。
状况 | amended | 说明 |
---|---|---|
新建sync.Map{} | false | |
第1次Store | true | 第一次Store后,dirty有1个值,read为0 |
第1次Load读 | false | 第一次读,m.misses为1,此时len(dirty)为1,len(dirty)>=misses时候,会进行第一次同步,同步完,dirty和read数据一致,此时amended改成false |
第2次Store | true | 第2次Store后,dirty有2个值,read为1,此时amended改成true,标记他们有差异 |
第n次 | ----- | ----- |
type entry struct {
p atomic.Pointer[any] // p 泛型指针
}
这里的sync.Map里面存储的entry,有三种值的情况。
- nil 空值
- expunged 标记空泛型new(any),代表被删除的泛型值。
- any 任意类型的非nil有效值。
当我们从dirty复制到read的时候,他们两个map,都指向同一个&entry,这样在后面我们就可以通过cas修改值,修改后,无论从read还是dirty读取,都会是新的值
type Pointer[T any] struct {
// Mention *T in a field to disallow conversion between Pointer types.
// See go.dev/issue/56603 for more details.
// Use *T, not T, to avoid spurious recursive type definition errors.
_ [0]*T
_ noCopy
v unsafe.Pointer
}
atomic.Pointer常用的方法有:
// 原子化加载 指针的值
func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&x.v)) }
// 原子化存储,指针的值
func (x *Pointer[T]) Store(val *T) { StorePointer(&x.v, unsafe.Pointer(val)) }
// 原子化,交换指针的值
func (x *Pointer[T]) Swap(new *T) (old *T) { return (*T)(SwapPointer(&x.v, unsafe.Pointer(new))) }
// cas
func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
return CompareAndSwapPointer(&x.v, unsafe.Pointer(old), unsafe.Pointer(new))
}
什么是CAS?
CAS全称compare and swap——比较并替换,它是并发条件下实现原子操作,修改数据的一种机制,包含三个操作数:
- 需要修改的数据的内存地址(V);
- 对这个数据的旧预期值(A);
- 需要将它修改为的值(B);
3.2 Store存储 key/value数据
语法如下:
func (m *Map) Store(key, value any)
在Store的时候,其实有关于key有2种情况需要我们考虑。
Store情况 | 处理方式 |
---|---|
未存在的key | m.dirty[key] = newEntry(value) |
已经存在的key | 具体分析如下表格 |
已经存在的key,我们需要考虑他的值指针的情况,有2种情况。
已经存在的key | 处理方式 |
---|---|
只在dirty中,还没同步到read | 修改对应key的值,e.swapLocked(&value) |
同时存在dirty和read中 | 备注他们的是同一个&entry地址,cas修改,方法e.trySwap(&value) |
我们先看一下Store的逻辑
接着我们看Store的源码实现
// Store sets the value for a key.
// Store 存储指定key的值
func (m *Map) Store(key, value any) {
_, _ = m.Swap(key, value)
}
// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
read := m.loadReadOnly()
// 1、key在 read map中, 更换key对应的值指针地址。
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
// 走到这里的情况为,1 不在read中 2在read中,不过被删除了。
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
// 1、如果key在 read map 存在,并且被删除了
// read map 中 这个key 对应的值为 nil
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// 该 entry实体记录 先前已被删除,
// non-nil dirty map and this entry is not in it.
// 意味着 dirty map 不为nil,且 这个 entry 不在里面。
m.dirty[key] = e
}
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else if e, ok := m.dirty[key]; ok {
// 2、如果key在 dirty map 存在
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {
// 3、key不在read和dirty map中 ,一个新key
if !read.amended {
// 如果 dirty中没有新key
// We're adding the first new key to the dirty map.
// 第一次 增加新key 到 dirty map
// Make sure it is allocated and mark the read-only map as incomplete.
// 重要! dirtyLocked 这个方法会初始化 dirty map
m.dirtyLocked()
// 把 amended 改为 true
m.read.Store(&readOnly{m: read.m, amended: true})
}
// 写入到 dirty map 中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}
3.3 Delete
语法为
func (m *Map) Delete(key any)
基本逻辑为:
源码请看演示:
// Delete deletes the value for a key.
// Delete 删除 指定key的值
func (m *Map) Delete(key any) {
m.LoadAndDelete(key)
}
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// LoadAndDelete 删除指定的key,返回先前的值。
// The loaded result reports whether the key was present.
// loaded 返回值,是key是否存在。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
// read中没有,且dirty map中的key后来新增过key
if !ok && read.amended {
m.mu.Lock()
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 删除dirty中的key
delete(m.dirty, key)
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
// miss增加1,直到 dirty map 下一次同步数据 到 read map
m.missLocked()
}
m.mu.Unlock()
}
// read 或 dirty map 存在的时候,把entry记录的指针 置为nil
if ok {
return e.delete()
}
return nil, false
}
3.4 range
语法:
func (m *Map) Range(f func(key, value any) bool) {
逻辑图:
源码演示:
func (m *Map) Range(f func(key, value any) bool) {
// We need to be able to iterate over all of the keys that were already
// present at the start of the call to Range.
// If read.amended is false, then read.m satisfies that property without
// requiring us to hold m.mu for a long time.
read := m.loadReadOnly()
if read.amended {
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
// (assuming the caller does not break out early), so a call to Range
// amortizes an entire copy of the map: we can promote the dirty copy
// immediately!
m.mu.Lock()
read = m.loadReadOnly()
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(&read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
3.5 Load
语法:
func (m *Map) Load(key any) (value any, ok bool)
逻辑图:
或者这样展示
源码阅读:
// Load returns the value stored in the map for a key, or nil if no
// Load方法 返回 map中key 存储的值,或者返回nil,如果现在没有值。
// value is present.
// The ok result indicates whether value was found in the map.
// ok返回值 表示 这个值是否存在map中。
func (m *Map) Load(key any) (value any, ok bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
// 当read中没有,但dirty map中有值的时候,从dirty读取一次。
if !ok && read.amended {
// 从dirty map 中读值,需要加锁。
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// 不管这条entry记录,是否存在,记录一次 未击中。m.misses 加一次
// will take the slow path until the dirty map is promoted to the read
// 这个key,会一直 走到这一个缓慢的逻辑中,直到dirty map升格到read map 中。
// map.
m.missLocked()
}
m.mu.Unlock()
}
// 当 dirty 和 read map中,都为false,没有这个key时候,直接返回。
if !ok {
return nil, false
}
// 当ok,有这个key的时候,返回read map中的值
return e.load()
}
3.6 LoadOrStore
语法:
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
逻辑图:
源码阅读:
// LoadOrStore returns the existing value for the key if present.
// LoadOrStore 返回指定key现在存在的值
// Otherwise, it stores and returns the given value.
// 或者,可以存储并且返回指定的值
// The loaded result is true if the value was loaded, false if stored.
// loaded返回值为 如果是load读取返回true,如果是store返回false
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
// Avoid locking if it's a clean hit.
// 如果是直接的命中read map,可以不用加锁
read := m.loadReadOnly()
if e, ok := read.m[key]; ok {
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
// 未命中 read map
m.mu.Lock()
read = m.loadReadOnly()
if e, ok := read.m[key]; ok {
// key在read map,但值被删除了,写入到dirty map中
if e.unexpungeLocked() {
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
// 未命中read map,命中dirty map
actual, loaded, _ = e.tryLoadOrStore(value)
// 未击中read map,命中dirty map,m.misses 增加1次
m.missLocked()
} else {
// 未命中read和dirty map。
if !read.amended {
// We're adding the first new key to the dirty map.
// 增加第一个 key 到 dirty map
// Make sure it is allocated and mark the read-only map as incomplete.
// 确保 read map 的 amended 标记未 为true (代表dirty相对read有差异,增加了新key)
m.dirtyLocked()
m.read.Store(&readOnly{m: read.m, amended: true})
}
// 新增的key 保存到 dirty map (全量map)中
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock()
return actual, loaded
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。