Go语言同步原语与数据竞争:Mutex 与 RWMutex
在Go语言并发程序中,当多个 goroutine 同时读写同一共享变量时,如果不加以控制,会引发数据竞争(race condition),导致程序行为不可预期。
Go 提供了多种同步原语,最基本的就是 sync.Mutex
和 sync.RWMutex
。
一、什么是数据竞争?
数据竞争发生在两个或多个 goroutine 同时访问相同的内存地址,并且至少有一个是写操作,且未使用同步机制。
表现包括:
- • 输出错乱、值错误
- • 程序崩溃或逻辑失常
- • 非确定性 bug(最难排查)
二、sync.Mutex:互斥锁
sync.Mutex
是最常见的锁,用于保护临界区,使得同一时间只有一个 goroutine 可以进入。
示例:未加锁的并发写入(错误)
var counter intfunc increment() {for i := 0; i < 1000; i++ {counter++}
}func main() {for i := 0; i < 10; i++ {go increment()}time.Sleep(time.Second)fmt.Println("Counter:", counter) // 不确定输出
}
正确使用 Mutex 加锁
var (counter intmutex sync.Mutex
)func increment() {for i := 0; i < 1000; i++ {mutex.Lock()counter++mutex.Unlock()}
}
三、sync.RWMutex:读写互斥锁
相比 Mutex
,sync.RWMutex
提供了更细粒度的控制:
操作 | 行为说明 |
Lock() | 获取写锁,所有读写都被阻塞 |
RLock() | 获取读锁,允许多个同时读 |
写锁优先级高 | 有写者等待时,新的读锁会被阻止 |
示例:多个读协程同时访问共享资源
type SafeMap struct {data map[string]stringmu sync.RWMutex
}func (s *SafeMap) Get(key string) string {s.mu.RLock()defer s.mu.RUnlock()return s.data[key]
}func (s *SafeMap) Set(key, val string) {s.mu.Lock()defer s.mu.Unlock()s.data[key] = val
}
四、使用场景对比
场景 | 推荐使用 |
多个读协程 + 偶尔写 | RWMutex |
频繁读写,互斥访问临界区 | Mutex |
极端并发 + 只读数据 | 无需加锁(只读) |
精确锁粒度对性能有重要影响的系统 | RWMutex 可调优 |
五、避免死锁的建议
- • 加锁后一定记得解锁,推荐使用
defer
- • 避免多个锁嵌套加锁(容易形成死锁环)
- • 尽量缩小加锁范围,不要锁整个函数
- • 避免在持锁状态下调用外部函数(可能阻塞)
六、使用 -race 检查数据竞争
Go 提供了内置的竞态检测工具:
go run -race main.go
可自动分析是否存在数据竞争,是并发调试的利器。
七、小结
- •
sync.Mutex
是最基础的并发控制工具,适用于串行化访问。 - •
sync.RWMutex
适用于读多写少的场景,提升并发效率。 - • 使用锁时一定要小心死锁与性能瓶颈,配合
-race
工具排查隐患。