编程后端开发者

在 Go 中有哪些同步的变种?如何使用 sync.Mutex、sync.RWMutex 和 sync.WaitGroup,以及每种情况有什么细微差别?

用 Hintsage AI 助手通过面试

答案

在 Go 中,通过 sync 包中的结构来同步并发的 goroutine,最常用的是 sync.Mutexsync.RWMutexsync.WaitGroup

sync.Mutex 提供了访问共享数据的互斥机制。它的方法包括 Lock()(锁定)和 Unlock()(解锁)。

sync.RWMutex 扩展了普通互斥锁的功能:允许并行读取,但修改时需要独占。

sync.WaitGroup 用于等待一组 goroutine 的完成。通过 Add(int)Done()Wait() 方法,您可以管理并发工作的生命周期。

例如:

var mu sync.RWMutex data := 0 // 读取 mu.RLock() fmt.Println(data) mu.RUnlock() // 写入 mu.Lock() data = 42 mu.Unlock()

细微差别:

  • 始终在 defer 块中使用 Unlock,以确保即使在 panic 的情况下也能解锁互斥锁。
  • 确保 WaitGroup.Done() 的调用次数与 Add() 对应。
  • 不要复制 WaitGroup、Mutex 和 RWMutex!

具有陷阱的问题

在同一个 goroutine 中是否可以连续两次捕获同一个 sync.Mutex(或 RWMutex)?会发生什么?

答案:不可以,如果您在同一个 Mutex 上连续调用 Lock() 而没有中间的 Unlock(),该 goroutine 将永远被阻塞(死锁)。在 Go 中,互斥锁不是递归的。

示例:

var mu sync.Mutex mu.Lock() // ... mu.Lock() // 死锁:将永远被阻塞,因为同一线程已经持有锁

真实错误的示例


故事

在一个高负载 REST API 项目中,开发者用一个互斥锁包裹了整个请求处理。这导致性能急剧下降——只能同时处理一个请求,尽管计划要服务成千上万的客户。原因是对 Mutex 和 RWMutex 之间的区别缺乏了解,并忽视了并行读取。


故事

在团队中的一位成员意外地将包含 Mutex 的结构体的副本传递给了另一个函数。这导致出现 "sync: copy of sync.Mutex" 的 panic 消息,以及在高负载下生产环境的崩溃。


故事

在使用 WaitGroup 时,忘记在多个 goroutine 中调用 Done(),这导致 Wait() 永远等待,从而阻塞主线程。结果是服务在手动重启之前失去了可用性。