Go
标准库中的map
(map
的使用及其底层实现原理可参考 Go数据结构之:映射map)并不是并发安全的,通常需要使用互斥锁(sync.Mutex
或sync.RWMutex
)或者改用sync.Map
。sync.Map
底层借助sync.Mutex
、读写分离的设计,减少加锁时机与粒度,非常适用于读多写少的场景,本文将从sync.Map
的使用、底层实现两个角度对其进行详细介绍(基于Go 1.16
版本)。
1 基础知识
1.1 基本使用
// sync.Map主要对外提供的API
func (m *Map) Store(key, value interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) Delete(key interface{})
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Range(f func(key, value interface{}) bool)
-
Store
:用于存储键值对,如果key
已经存在,则更新其对应的值。 -
Load
:加载key
对应的值,-
如果
key
存在,返回的ok
为true
,value
为key
对应的值; -
如果
key
不存在,则ok
为false
,value
为nil
。
-
-
Delete
:删除键值对,如果key
不存在,则不执行任何操作。 -
LoadAndDelete
:尝试加载并删除键值对,-
如果
key
存在,loaded
为true
,删除该键值对并且返回其原来的值; -
如果
key
不存在,loaded
为false
,不执行任何操作,value
为nil
。
-
-
LoadOrStore
:尝试加载key
,-
如果
key
存在,loaded
为true
,返回已经存在的值 -
如果
key
不存在,loaded
为false
,存储给定的键值对,并返回这个vlaue
-
-
Range
:遍历sync.Map
中所有有效的键值对,对每一个键值对调用传入的函数f,一旦调用f
返回了false
,整个遍历过程后离开停止。需要注意的是,此处的遍历其实只是键值对集合的某一个一致性快照,遍历过程中发生的Store
、Delete
操作可能被反映在这次遍历中,也可能不被反映(即遍历可能包含新写入的键值对,也可能不包含刚删除的键值对,因为遍历的对象是read map
)。
func main() {
// 直接声明变量后即可使用
var sm sync.Map
// 存储,如果最先调用的是Store函数,则底层会先初始化dirty map
sm.Store("name", "Alice")
sm.Store("age", 30)
// 加载
if name, ok := sm.Load("name"); ok {
fmt.Println("Name:", name) // 输出: Name: Alice
}
// LoadOrStore (存在)
if actual, loaded := sm.LoadOrStore("name", "Bob"); loaded {
fmt.Println("Name already exists:", actual) // 输出: Name already exists: Alice
}
// LoadOrStore (不存在)
if actual, loaded := sm.LoadOrStore("job", "Engineer"); !loaded {
fmt.Println("Stored new job:", actual) // 输出: Stored new job: Engineer
}
// LoadAndDelete
if age, loaded := sm.LoadAndDelete("age"); loaded {
fmt.Println("Deleted age:", age) // 输出: Deleted age: 30
}
// 遍历 (注意快照特性)
sm.Range(func(k, v interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", k, v)
// 可能输出:
// Key: name, Value: Alice
// Key: job, Value: Engineer
return true // 继续遍历
})
}
1.2 性能测试
针对实际开发中常用的三种map
并发安全方案:
-
原生
map
+sync.Mutex
-
sync.Map
-
原生
map
+sync.RWMutex
(针对读多写少优化)
编写测试代码,进行如下测试场景包括:
- 纯读:100%读,0%写
- 读写比例9:1:90%读,10%写
- 读写比例1:1:50%读,50%写
- 纯写:0%读,100%写
package main
import (
"math/rand"
"sync"
"testing"
)
const (
operationCount = 1000000 // 每个测试的总操作数
keySpaceSize = 10000 // 键空间大小
)
// 方案1: 原生 map + sync.Mutex
type MutexMap struct {
mu sync.Mutex
m map[int]int
}
func NewMutexMap() *MutexMap {
return &MutexMap{m: make(map[int]int)}
}
func (mm *MutexMap) Store(k, v int) {
mm.mu.Lock()
defer mm.mu.Unlock()
mm.m[k] = v
}
func (mm *MutexMap) Load(k int) (int, bool) {
mm.mu.Lock()
defer mm.mu.Unlock()
v, ok := mm.m[k]
return v, ok
}
// 方案2: 原生 map + sync.RWMutex (读写分离)
type RWMutexMap struct {
mu sync.RWMutex
m map[int]int
}
func NewRWMutexMap() *RWMutexMap {
return &RWMutexMap{m: make(map[int]int)}
}
func (rw *RWMutexMap) Store(k, v int) {
rw.mu.Lock()
defer rw.mu.Unlock()
rw.m[k] = v
}
func (rw *RWMutexMap) Load(k int) (int, bool) {
rw.mu.RLock()
defer rw.mu.RUnlock()
v, ok := rw.m[k]
return v, ok
}
// 方案3: sync.Map (标准库并发安全Map)
type SyncMap struct {
m sync.Map
}
func NewSyncMap() *SyncMap {
return &SyncMap{}
}
func (sm *SyncMap) Store(k, v int) {
sm.m.Store(k, v)
}
func (sm *SyncMap) Load(k int) (int, bool) {
v, ok := sm.m.Load(k)
if !ok {
return 0, false
}
return v.(int), true
}
// 初始化测试数据
func initTestData(initSize int) []int {
keys := make([]int, initSize)
for i := 0; i < initSize; i++ {
keys[i] = i
}
rand.Shuffle(initSize, func(i, j int) {
keys[i], keys[j] = keys[j], keys[i]
})
return keys
}
// 基准测试函数
func benchmarkMap(b *testing.B, readPercent int, setupFunc func() interface {
Store(k, v int)
Load(k int) (int, bool)
}) {
keys := initTestData(keySpaceSize)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
// 每个goroutine的本地随机源,避免锁竞争
r := rand.New(rand.NewSource(rand.Int63()))
mapInstance := setupFunc()
for pb.Next() {
op := r.Intn(100)
key := keys[r.Intn(len(keys))]
if op < readPercent {
// 读操作
mapInstance.Load(key)
} else {
// 写操作
mapInstance.Store(key, r.Intn(1000))
}
}
})
}
// 纯读场景 (100%读)
func BenchmarkMutexMap_Read100(b *testing.B) {
benchmarkMap(b, 100, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
m := NewMutexMap()
// 预填充数据
for i := 0; i < keySpaceSize; i++ {
m.Store(i, i)
}
return m
})
}
func BenchmarkRWMutexMap_Read100(b *testing.B) {
benchmarkMap(b, 100, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
m := NewRWMutexMap()
for i := 0; i < keySpaceSize; i++ {
m.Store(i, i)
}
return m
})
}
func BenchmarkSyncMap_Read100(b *testing.B) {
benchmarkMap(b, 100, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
m := NewSyncMap()
for i := 0; i < keySpaceSize; i++ {
m.Store(i, i)
}
return m
})
}
// 读写比例 9:1 (90%读, 10%写)
func BenchmarkMutexMap_Read90(b *testing.B) {
benchmarkMap(b, 90, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewMutexMap()
})
}
func BenchmarkRWMutexMap_Read90(b *testing.B) {
benchmarkMap(b, 90, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewRWMutexMap()
})
}
func BenchmarkSyncMap_Read90(b *testing.B) {
benchmarkMap(b, 90, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewSyncMap()
})
}
// 读写比例 1:1 (50%读, 50%写)
func BenchmarkMutexMap_Read50(b *testing.B) {
benchmarkMap(b, 50, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewMutexMap()
})
}
func BenchmarkRWMutexMap_Read50(b *testing.B) {
benchmarkMap(b, 50, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewRWMutexMap()
})
}
func BenchmarkSyncMap_Read50(b *testing.B) {
benchmarkMap(b, 50, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewSyncMap()
})
}
// 纯写场景 (0%读, 100%写)
func BenchmarkMutexMap_Write100(b *testing.B) {
benchmarkMap(b, 0, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewMutexMap()
})
}
func BenchmarkRWMutexMap_Write100(b *testing.B) {
benchmarkMap(b, 0, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewRWMutexMap()
})
}
func BenchmarkSyncMap_Write100(b *testing.B) {
benchmarkMap(b, 0, func() interface {
Store(k, v int)
Load(k int) (int, bool)
} {
return NewSyncMap()
})
}
从上述测试代码的基准测试结果来看,随着写比例的增加,sync.Map
的性能急剧下降,因为读操作较多的情况下,首先是通过无锁化进行取值操作;而随着写操作的增加,read map与dirty map之间的转换频率增加,由此导致使用性能下降。
go test -bench=. -benchmem tt_test.go
goos: darwin
goarch: arm64
BenchmarkMutexMap_Read100-8 122276505 9.637 ns/op 0 B/op 0 allocs/op
BenchmarkRWMutexMap_Read100-8 124338636 9.669 ns/op 0 B/op 0 allocs/op
BenchmarkSyncMap_Read100-8 89099803 13.42 ns/op 0 B/op 0 allocs/op
BenchmarkMutexMap_Read90-8 121638187 10.08 ns/op 0 B/op 0 allocs/op
BenchmarkRWMutexMap_Read90-8 120444948 10.00 ns/op 0 B/op 0 allocs/op
BenchmarkSyncMap_Read90-8 66396577 17.28 ns/op 3 B/op 0 allocs/op
BenchmarkMutexMap_Read50-8 100000000 10.55 ns/op 0 B/op 0 allocs/op
BenchmarkRWMutexMap_Read50-8 100000000 10.74 ns/op 0 B/op 0 allocs/op
BenchmarkSyncMap_Read50-8 46445742 25.65 ns/op 15 B/op 1 allocs/op
BenchmarkMutexMap_Write100-8 100000000 10.40 ns/op 0 B/op 0 allocs/op
BenchmarkRWMutexMap_Write100-8 112851788 10.74 ns/op 0 B/op 0 allocs/op
BenchmarkSyncMap_Write100-8 27792708 36.73 ns/op 30 B/op 2 allocs/op
2 底层实现
2.1 sync.Map 数据结构
sync.Map
的底层数据结构包含
-
mu
,该互斥量只有在操作dirty map
时才需要上锁,对于read map
的操作一般只涉及到原子操作。 -
read map
,读取操作优先从该map
中进行判断,该map
中,值可能出现expunged
、nil
、0x...
三种类型,其中expunged
表示上一次进行read
===>dirty
转换时,已经被删除(nil ===> expunged
),这类键值对就不会被写入到dirty中,
等待下一次dirty
===>read
时,就会被彻底删除。 -
dirty map
,包含Map
全部的键值对,该map
中的值只会出现nil
、0x...
两种。这里需要注意的是,read
、dirty
两个map
底层value
指向的内存地址其实是同一个(expunged
类型除外),也就是说通过任意一个map
修改地址的value
值,两个map
都能感知到。 -
misses
,用于read
中获取失败次数的计数,如果misses
值大于等于dirty
的长度,则会触发dirty
===>read
的转换。
type Map struct {
// 互斥锁,用于保证map的并发安全
mu Mutex
read atomic.Value // readOnly
// 包含read map(除了value为expunged)+ 新插入的key-value
dirty map[interface{}]*entry
// 用于控制是否需要将dirty ==> read
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
// 标记是否有新的键值对存在于dirty map 而不存在于 readOnly.m
amended bool // true if the dirty map contains some key not in m.
}
// An entry is a slot in the map corresponding to a particular key.
type entry struct {
// p 代表的是执行value存储的指针,该变量一共有三个值
// p == nil,标记该entry已经被删除,只是还没进行read ==> dirty 同步
// p == expunged,表示正在进行read==>dirty转换,此时会将read中p = nil的键值对转换为
// expunged,新的ditry中不会在出现该key,等下一次dirty ==> read,之前被删除的key就会彻底被移除
// p == 0x...,表示键值对有效,该指针执行具体的内存地址
p unsafe.Pointer // *interface{}
}
2.2 Store函数
-
第一步(乐观 - 无锁):尝试从
read
中加载readOnly
并查找key
对应的entry
。-
如果找到
e
并且e.p
不是expunged
,尝试原子地更新entry.p
指向新的值。如果 CAS 成功,直接返回(因为非expunged的键值对同时存在于read、dirty
)。这是更新已存在 key 的高效路径(无锁)! -
如果找到
e
但e.p
是expunged
,说明这个 key 之前在read
中被标记为完全删除(dirty
里也没有了),不能直接更新(如果直接更新,dirty
中并没有这个key,违背了dirty包含全部生效的键值对原则
),需要进入慢路径。 -
如果没找到,且
readOnly.amended
为false
,说明dirty
是空的(或者还没初始化),也进入慢路径。
-
-
第二步(加锁 - 慢路径):获取
mu
锁(在操作结束后及时释放)。-
再次检查
read
(Double-Checking):避免在加锁期间read
已经被更新了。如果这次在read
中找到了e
:-
如果
e.p
是expunged
:需要将这个 key 重新“复活”到dirty
中。将e.p
设置为nil
(临时状态),然后向dirty
map 添加key: e
。接着就可以更新e.p
指向新值了。 -
如果
e.p
不是expunged
(可能被其他 goroutine 更新了):直接用 CAS 更新e.p
(和第一步的快速更新类似)。
-
-
如果在
read
中没找到key
:-
如果
dirty
为nil
,需要初始化dirty
:创建一个新的 map,并将当前read.m
中所有p != nil && p != expunged
(nil、expunged表示该键值对已经被删除了,如果为nil,此时需要将其在read中的状态修改为expunged
)的entry
浅拷贝到新dirty
中(值是指针拷贝,不是值拷贝)。同时设置readOnly.amended = true
。 -
如果
dirty
非nil
,且readOnly.amended
已经是true
,直接操作dirty
。 -
将
key: newEntry(value)
添加到dirty
map 中(如果 key 已存在则更新对应的 entry)。 -
更新
read
中对应的entry
(如果存在且是expunged
状态需要先复活)或者确保dirty
包含了这个 key,并设置readOnly.amended = true
。
-
-
更新或创建
entry
,设置entry.p
指向新值。
-
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
// 如果key存在于read中,且并没有被标记为expunged,
// 即同时存在于read和dirty中,则直接使用原子操作进行更新(避免的对dirty修改所需的加锁操作)
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 为防止上一次m.read.Load()与m.mu.Lock()之间,有其他并发操作,需要再一次判断m.read
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
// 1. key 存在于read中
// 1.1 key仅存在于read中,既 p == expunged 的情况
// 1.2 key同时存在于read、dirty中
if e, ok := read.m[key]; ok {
// key仅存在于read中,则需要将其加入到dirty中
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// 2. key仅存在于dirty
e.storeLocked(&value)
} else {
// 3. key不存在于read、dirty,需要重新插入
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
// dirty == nil,且新加入的key不存在于read中,则将read ==> dirty
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
2.3 Load函数
-
第一步(无锁):首先原子地加载
read
指向的readOnly
结构 (readOnly
本身是只读的,所以并发读安全)。尝试从readOnly.m
这个 map 中通过key
查找对应的*entry
。-
如果找到
e
,并且e.p
不是nil
也不是expunged
(即有效状态),直接返回e.p
指向的值;e.p
是nil
或者expunged
(即无效状态),直接返回 nil。这是最快速、无锁的路径! -
如果在
readOnly.m
没找到key
,并且readOnly.amended
为true
(表示dirty
中可能有新数据),则进入慢路径。
-
-
第二步(加锁 - 慢路径):获取
mu
锁。-
再次检查
read
(Double-Checking):避免在加锁期间read
已经被更新了。如果这次在read
中找到了有效的entry
(可能其他 goroutine 刚写入或提升了),解锁并返回。 -
检查
dirty
:如果read
中还是没有找到且dirty
非nil
,尝试从dirty
map 中找key
。-
记录一次
miss
(m.misses++
),如果misses
计数 >=len(dirty)
,触发dirty
提升为新的read
(read
指向新的readOnly
,其m
设置为当前的dirty
,amended
设置为false
),然后将dirty
置为nil
,misses
重置为 0。最后解锁并返回dirty
中找到的值。 -
如果
dirty
中也没找到,解锁并返回nil, false
。
-
-
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 先不加锁,从read中查找
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// read中不存在,且dirty中存在新增的值
if !ok && read.amended {
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.)
// 避免并发时,在上一次读 m.read.Load() 和 m.mu.Lock() 的时间区间内发生 dirty=>read 的升级
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended { // 只有 read.amended 为true时,m.dirty才不为nil
e, ok = 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.
// 判断是否需要将dirty ==> read
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
2.4 Delete/LoadAndDelete函数
-
类似
Load
,先尝试从read
中无锁查找:-
如果找到
e
且e.p
非nil
且非expunged
,尝试用 CAS 将e.p
设置为nil
(标记为逻辑删除)。如果成功,返回(对于LoadAndDelete
返回原值)。 -
如果
e.p
是nil 或 expunged
,说明它已经被删除了,返回nil,flase
。
-
-
如果快速路径失败,进入加锁慢路径:
-
加锁后再次检查
read,
如果在read
中找到e
:-
如果
e.p
非nil
且非expunged
,将其设置为nil
(逻辑删除)。 -
如果
e.p
是nil 或 expunged
,说明它已经被删除了,返回nil,flase
。
-
-
如果在
read
没找到且amended
为true
,且dirty
非nil
,则直接从dirty
中删除key
(如果存在的话,直接进行的是物理删除)并返回对应的值(对于LoadAndDelete
)。
-
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
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]
// 对于仅存在于dirty map中的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.
m.missLocked()
}
m.mu.Unlock()
}
// 存在于read map的的key执行的是逻辑删除,既将value值修改为nil
if ok {
return e.delete()
}
return nil, false
}
2.5 Range函数
-
首先,检查
read map
中是否有数据(m.read.Load()
获取readOnly
结构)。如果read map
已经包含了当前的所有键(即dirty
为nil
),那么我们可以安全地遍历read map
,因为read map
是原子值且只读,遍历过程中不会发生并发问题。 -
如果
dirty
不为nil
,说明可能存在read map
中没有的新键,此时需要加锁,然后将dirty
提升为read
(将m.dirty
赋值给m.read
,并将m.dirty
置为nil
,同时更新m.misses
为0)。 -
然后遍历新的
read map
(即原来的dirty map
),并对其中的每个键值对调用函数f
。注意:在遍历过程中,如果f
返回false
,则立即终止遍历。在遍历过程中,由于read map是无锁操作,
可能会出现键被删除的情况(表现为值被标记为expunged
或nil
),这类在遍历过程中被删除的键值对,如果在获取到该值之前被标记,则会被忽略;如果在获取到该值之后被标记,则会正常被读取。
需要注意的是,在遍历过程中,如果有新的键值对被添加,由于在开始遍历时dirty
已经被提升为read
,新的写入会进入新的dirty
(尚未创建),而当前的read
在遍历期间不会变化(只读),所以不会影响当前遍历。但在遍历开始之后新加入的键值对将不会出现在本次遍历中。另外,在遍历过程中,如果遇到已经被删除的键(即值被标记为expunged
)或者nil
,则跳过。
func (m *Map) Range(f func(key, value any) bool) {
// 首先,获取当前的read map(只读)
read := m.loadReadOnly()
// 如果dirty中有read中没有的键,则需要将dirty提升为read,然后遍历新的read
if read.amended {
// 加锁,确保并发安全
m.mu.Lock()
// 再次获取read,因为锁之前可能read已经被更新
read = m.loadReadOnly()
if read.amended {
// 将dirty提升为read
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
// 遍历read map(只读结构)
for k, e := range read.m {
v, ok := e.load()
if !ok {
// 如果该entry已经被删除,则跳过
continue
}
// 调用f函数,如果f返回false,则终止遍历
if !f(k, v) {
break
}
}
}
2.6 read与dirty之间的转换
dirty
提升为 read
(提升操作):
- 时机:
- 当执行
Range
操作并且amended
为true
(表示dirty
包含read
中没有的键)时,需要先提升dirty
为read
以获取最新数据的快照。 - 当
Load
操作在read
中未找到键(miss
)并且需要访问dirty
时,如果misses
计数超过了dirty
的长度(len(m.dirty)
),也会触发提升操作。这确保在多次未命中后,最新的dirty
数据被提升为read
,减少后续的锁争用。
- 当执行
- 目的:提升后,
dirty
被置为nil
,misses
被重置为0。这样新的读操作可以直接在read
中查找而不需要加锁,直到新的写操作再次创建dirty
。
read
重建为 dirty
(重建操作):
- 时机:当需要写(
Store
)一个不在dirty
中的新键,且此时dirty
为nil
时(通常是上次提升后)。 - 过程:
- 将
read
中未被标记为删除(nil
、expunged
)的键值对复制到新创建的dirty
映射。 - 在复制过程中,若
read
中的某些项被删除(nil
、expunged,nil更新成expunged
),则跳过;否则将其标记为正常状态(非删除状态),然后复制到dirty
。
- 将
- 目的:重建后,新写入的键值对可以安全地添加到
dirty
中。同时,标记amended=true
表示dirty
包含read
中没有的额外数据。