跳到主要内容

sync 包

问题

Go 的 sync 包提供了哪些并发原语?Mutex 和 RWMutex 怎么选?sync.Map 适合什么场景?

答案

sync.Mutex——互斥锁

var mu sync.Mutex
var count int

func increment() {
mu.Lock()
defer mu.Unlock()
count++
}
Mutex 注意事项
  • 零值可用(不需要初始化)
  • 不可重入(同一 goroutine 重复 Lock 会死锁)
  • 不可拷贝(传参/赋值用指针)
  • Lock() 后必须 Unlock(),推荐用 defer

sync.RWMutex——读写锁

读写锁允许多个读者同时读,但写者独占

var rw sync.RWMutex
var cache map[string]string

func get(key string) string {
rw.RLock() // 读锁(多个可同时持有)
defer rw.RUnlock()
return cache[key]
}

func set(key, val string) {
rw.Lock() // 写锁(独占)
defer rw.Unlock()
cache[key] = val
}
操作MutexRWMutex
读-读互斥并发
读-写互斥互斥
写-写互斥互斥

适用场景:读多写少时用 RWMutex,否则用 Mutex(RWMutex 开销略高)。

sync.WaitGroup——等待组

var wg sync.WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // 等价于 wg.Add(-1)
doWork(id)
}(i)
}

wg.Wait() // 阻塞直到计数器归零
WaitGroup 注意事项
  • Add 必须在启动 goroutine 之前调用(不能在 goroutine 内部 Add)
  • Add 的总数必须和 Done 的总数一致
  • 计数器不能为负数(panic)

sync.Once——只执行一次

var once sync.Once
var instance *DB

func GetDB() *DB {
once.Do(func() {
instance = connectDB() // 只会执行一次,并发安全
})
return instance
}

sync.Once 保证:

  • 函数只执行一次,即使多个 goroutine 并发调用
  • 其他 goroutine 会等待第一次执行完成后才返回
  • 如果函数 panic,Once 仍然认为已执行(不会重试)

sync.Map——并发安全 Map

var m sync.Map

// 存储
m.Store("key", "value")

// 读取
val, ok := m.Load("key")

// 读取或存储(key 不存在时存储)
actual, loaded := m.LoadOrStore("key", "default")

// 删除
m.Delete("key")

// 遍历
m.Range(func(key, value any) bool {
fmt.Println(key, value)
return true // 返回 false 停止遍历
})

// Go 1.20+:LoadAndDelete、Swap、CompareAndSwap、CompareAndDelete

sync.Map 适用场景(内部用两个 map + atomic 实现):

  • 读多写少:key 一旦写入很少更新
  • key 分区:不同 goroutine 操作不同 key

不适用场景(用 RWMutex + map 更好):

  • 写操作频繁
  • key 集合变化大

sync.Cond——条件变量

var mu sync.Mutex
cond := sync.NewCond(&mu)
queue := make([]int, 0)

// 生产者
go func() {
for i := 0; i < 10; i++ {
mu.Lock()
queue = append(queue, i)
cond.Signal() // 唤醒一个等待者
mu.Unlock()
}
}()

// 消费者
go func() {
for {
mu.Lock()
for len(queue) == 0 {
cond.Wait() // 等待信号(自动释放锁,收到信号后重新获取)
}
item := queue[0]
queue = queue[1:]
mu.Unlock()
process(item)
}
}()
方法说明
Wait()释放锁,挂起等待,被唤醒后重新获取锁
Signal()唤醒一个等待者
Broadcast()唤醒所有等待者
实际开发中 Channel 通常比 Cond 更好用

sync.Cond 使用复杂且容易出错。大多数场景可以用 Channel 替代。Cond 的优势在于 Broadcast 唤醒所有等者,如果需要这个语义且频繁触发,Cond 比每次关闭 Channel 更高效。

sync.Pool——对象池

var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}

func process() {
buf := bufPool.Get().(*bytes.Buffer) // 获取(可能复用旧对象)
defer func() {
buf.Reset()
bufPool.Put(buf) // 归还
}()

buf.WriteString("hello")
// 使用 buf...
}

sync.Pool 特点:

  • 对象可能被 GC 清理(不是持久缓存)
  • 用于减少高频临时对象的内存分配和 GC 压力
  • 标准库中 fmt.Fprintfencoding/json 等大量使用

常见面试问题

Q1: Mutex 是可重入的吗?

答案不是。Go 的 sync.Mutex 不是可重入锁(Recursive Lock)。同一个 goroutine 对已锁定的 Mutex 再次 Lock 会死锁

mu.Lock()
mu.Lock() // 死锁!当前 goroutine 永久阻塞

Go 故意不提供可重入锁,因为:如果你需要重入,说明代码设计有问题——应该将锁的粒度调整为不需要重入。

Q2: RWMutex 的写锁饥饿问题?

答案

Go 的 RWMutex 实现了写优先策略——当有写锁等待时,新的读锁请求会被阻塞,防止读锁饥饿写锁。

但如果读操作极其频繁,写锁仍然可能长时间等待。

Q3: WaitGroup 和 Channel 等待 goroutine 完成哪个好?

答案

场景sync.WaitGroupChannel
只等待完成,不收集结果✅ 简单直接需要额外代码
需要收集每个 goroutine 的结果需配合其他机制✅ 自然支持
需要错误处理errgroup可以
动态数量的 goroutine可以

通常 WaitGroup 更简单,errgroup(基于 WaitGroup)更适合需要错误收集的场景。

Q4: sync.Pool 的对象什么时候被清理?

答案

每次 GC 时,sync.Pool 中的对象可能被全部清理。Go 1.13 优化了策略——引入 victim cache,GC 时先移到 victim,下次 GC 才真正清理,给了第二次机会。

所以 Pool 不适合做缓存(随时可能丢失),只适合做临时对象复用

相关链接