Sync(同步原语)
sync 和 sync/atomic 用于保护共享内存、等待任务完成或只执行一次初始化。选择原语前先确定共享状态、所有权和退出条件;并发安全不是由“看起来顺序执行”保证的。
Mutex 保护共享状态
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Add(delta int) {
c.mu.Lock()
defer c.mu.Unlock()
c.value += delta
}
片段需要导入 sync;完整程序还应通过构造函数或零值直接创建 Counter。互斥量应随被保护状态一起归属同一个对象。
锁应覆盖不变量所需的最小临界区。不要复制已经使用过的含锁结构体,也不要在锁内执行不可控的网络或文件 I/O。RWMutex 只有在读多写少且基准测试证明有收益时才值得增加复杂度。
等待、一次性初始化和原子操作
var (
startOnce sync.Once
startErr error
)
func start() error {
startOnce.Do(func() {
startErr = initialize()
})
return startErr
}
片段需要导入 sync,并由示例所在包提供 initialize 函数。WaitGroup 的使用也应放在拥有任务生命周期的函数中,而不是作为全局计数器。
WaitGroup 适合等待一组已知任务完成;应在启动 goroutine 前调用 Add,并在 goroutine 内 defer Done,不要复制 WaitGroup。Once 适合只执行一次初始化;初始化失败后,后续调用仍会看到保存的失败结果,若需要重试必须显式设计状态机。简单整数计数可用 sync/atomic,但原子读写不能自动保护多个字段组成的不变量。
验证并发假设
go test -race ./...
竞态检测针对未同步的并发读写;它不能证明没有死锁、活锁、饥饿或错误的任务取消。测试应设置超时并覆盖锁竞争、初始化失败和 goroutine 退出。
常见错误
- 忘记解锁,或在多个返回路径中手工解锁不完整。
- 把阻塞 I/O 放进锁的临界区。
- 在
Add前 goroutine 已运行,导致 WaitGroup 计数竞态。 - 复制 Mutex、RWMutex、Once 或 WaitGroup。
- 用 atomic 保护一个需要整体一致性的多字段状态。
- 为了“更快”使用 RWMutex,却没有 benchmark 证据。
channel 适合转移数据和建立同步关系;mutex/atomic 适合保护已经存在的共享状态。两者不是谁更高级的替代关系,应根据所有权和不变量选择。
小结
- Mutex 保护临界区,WaitGroup 等待任务,Once 执行一次,atomic 处理有限的原子状态。
- 所有同步原语都需要明确生命周期、复制规则和失败路径。
- 使用
go test -race与超时测试验证,而不是凭经验判断安全。