Golang 并发安全 Map 设计与实现详解
前言
当多个 goroutine 同时读写 map 时,会触发 runtime 的并发检测机制,导致 panic。
因此我们需要一个并发安全的 Map 结构。
背景
官方文档中明确说明:Map 不是并发安全的。
// 错误的并发使用示例
package main
import (
"fmt"
"sync"
)
func main() {
m := make(map[int]int)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m[n] = n // Spoiler: This blows up
}(i)
}
wg.Wait()
fmt.Println(m)
}
运行上述代码大概率会得到fatal error: concurrent map writes报错。
解决
引入 sync.RWMutex 实现读写锁机制:
- 读锁(RLock):允许多个 goroutine 同时读取
- 写锁(Lock):只允许一个 goroutine 进行写入
package main
import (
"fmt"
"sync"
)
// SafeMap is a concurrent-safe map protected by a RWMutex.
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
// NewSafeMap creates a new SafeMap.
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
// Get retrieves a value for a key with a read lock.
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock() // Acquire a read lock
defer sm.mu.RUnlock() // Release the read lock when the function returns
val, ok := sm.m[key]
return val, ok
}
// Set sets a value for a key with a write lock.
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock() // Acquire a write lock
defer sm.mu.Unlock() // Release the write lock when the function returns
sm.m[key] = value
}
// Delete removes a key from the map with a write lock.
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock() // Acquire a write lock
defer sm.mu.Unlock() // Release the write lock when the function returns
delete(sm.m, key)
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
// Multiple writers
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key%d", n), n)
}(i)
}
// Multiple readers
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Get(fmt.Sprintf("key%d", n))
}(i)
}
wg.Wait()
fmt.Println("Done without race!")
}
延伸
为什么不直接使用标准库?
Golang 1.9+ 提供了官方的并发安全 Map:
import "sync"
var m sync.Map
// 基本操作
m.Store("key", "value")
value, ok := m.Load("key")
m.Delete("key")
// 原子操作
m.LoadOrStore("key", "value")
m.LoadAndDelete("key")
而sync.Map内部由更复杂的HashTrieMap[any, any]实现,对比如下:
| 场景 | SafeMap | sync.Map | 胜出方 |
|---|---|---|---|
| 纯读操作 | O(1) + RLock 开销 | O(1) 无锁原子访问 | sync.Map |
| 纯写操作 | O(1) + Lock 开销 | O(1) + Lock + 可能复制 | SafeMap |
| 读多写少 | 读锁竞争 | 无锁读,写时复制 | sync.Map |
| 写多读少 | 频繁互斥锁 | 频繁 promote 和复制 | SafeMap |
| 内存占用 | 单个 map | 两个 map(空间换时间) | SafeMap |
总结
并发安全 Map 的实现体现了 Golang 并发编程的核心哲学:"通过通信共享内存,而不是通过共享内存进行通信"。
Do not communicate by sharing memory; instead, share memory by communicating.
我们使用了共享内存的方式,通过合理的锁机制,确保并发安全。
使用SafeMap当你需要:
- 写入频率较高/读写比例均衡
- 频繁遍历键值对/获取长度
- 内存敏感场景降低开销
使用sync.Map当你需要:
- 配置/缓存等读多写少的场景
- 不需要频繁遍历所有键值对
- 不同goroutine操作不同的键
记住,没有银弹。根据实际场景选择最合适的策略,才是优秀工程师的体现。
参考
Go maps in action - The Go Programming Language
The Go Memory Model - The Go Programming Language
Effective Go - The Go Programming Language
sync package - sync - Go Packages
Share Memory By Communicating - The Go Programming Language

浙公网安备 33010602011771号