Loading

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

posted @ 2026-01-31 23:06  azureology  阅读(3)  评论(0)    收藏  举报