在 Go 中,通过 sync 包中的结构来同步并发的 goroutine,最常用的是 sync.Mutex、sync.RWMutex 和 sync.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() 对应。在同一个 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() 永远等待,从而阻塞主线程。结果是服务在手动重启之前失去了可用性。