引入
多个 goroutine 同时读写同一个变量,结果可能每次运行都不一样——这就是竞态条件。并发安全是写 Go 程序绕不开的话题,Go 提供了 sync.Mutex、sync.Once、atomic 包等工具来保证并发场景下数据的正确性。
正文
定义
定义
竞态条件(race condition)指多个
goroutine同时访问共享数据,且最终结果依赖执行顺序。并发安全指程序在并发环境下依然能得到正确结果。Go 用sync.Mutex加锁、sync.Once保证只执行一次、atomic包做原子操作来保障并发安全。
语法
// sync.Mutex
var mu sync.Mutex
mu.Lock()
// 操作共享数据
mu.Unlock()
// sync.Once
var once sync.Once
once.Do(func() {
// 只执行一次
})
// atomic 原子操作
var count int64
atomic.AddInt64(&count, 1)
atomic.LoadInt64(&count)
atomic.StoreInt64(&count, 42)例子
竞态条件演示
func main() {
count := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // 不是原子操作:读→加→写
}()
}
wg.Wait()
fmt.Println(count) // 小于 1000(每次可能不同)
}count++ 看似一行,实际是读取、加一、写回三步,多个 goroutine 交错执行会丢失更新。
sync.Mutex 修复竞态
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count) // 1000
}加锁后同一时刻只有一个 goroutine 执行 count++,结果正确。
defer 解锁防止遗漏
func safeIncrement(mu *sync.Mutex, count *int) {
mu.Lock()
defer mu.Unlock() // 即使中间 panic 也能解锁
*count++
}用 defer mu.Unlock() 确保函数退出时一定解锁,避免死锁。
sync.Once 只执行一次
var once sync.Once
var config string
func loadConfig() {
once.Do(func() {
fmt.Println("加载配置...")
config = "loaded"
})
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
loadConfig()
}()
}
wg.Wait()
fmt.Println(config) // loaded("加载配置..." 只打印一次)
}sync.Once 保证 Do() 里的函数无论被调用多少次都只执行一次,适合初始化操作。
atomic 原子操作
var count int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1)
}()
}
wg.Wait()
fmt.Println(count) // 1000atomic.AddInt64() 是原子操作,不会被其他 goroutine 打断,比 Mutex 更轻量,适合简单的计数器场景。
atomic 常用操作
var val int64
atomic.StoreInt64(&val, 100) // 写入
fmt.Println(atomic.LoadInt64(&val)) // 读取:100
atomic.AddInt64(&val, 10) // 加
fmt.Println(atomic.LoadInt64(&val)) // 110
old := atomic.SwapInt64(&val, 0) // 交换,返回旧值
fmt.Println(old) // 110
ok := atomic.CompareAndSwapInt64(&val, 0, 50) // CAS
fmt.Println(ok, val) // true 50| 函数 | 说明 |
|---|---|
atomic.AddInt64 | 原子加法 |
atomic.LoadInt64 | 原子读取 |
atomic.StoreInt64 | 原子写入 |
atomic.SwapInt64 | 原子交换 |
atomic.CompareAndSwapInt64 | 原子 CAS(比较并交换) |
用 race 检测器找竞态
go run -race main.go
go test -race ./...Go 内置 race 检测器,编译时加 -race 就能在运行时检测数据竞争。开发阶段建议始终开启。
常见写法
| 写法 | 说明 |
|---|---|
mu.Lock() / defer mu.Unlock() | 加锁保护共享资源 |
once.Do(func() {…}) | 保证只执行一次 |
atomic.AddInt64(&v, 1) | 原子自增 |
atomic.LoadInt64(&v) | 原子读取 |
atomic.StoreInt64(&v, n) | 原子写入 |
atomic.CompareAndSwapInt64 | CAS 操作 |
go run -race | 检测数据竞争 |
特点
- 竞态条件源于多个
goroutine同时读写共享数据,结果不可预测 sync.Mutex是最通用的加锁方式,适合复杂操作defer mu.Unlock()是最佳实践,防止忘记解锁sync.Once保证函数只执行一次,适合懒加载、初始化atomic包提供原子操作,比Mutex更轻量,适合简单数值操作atomic只能操作 int32/int64/uint32/uint64/uintptr/Pointer 等类型-race检测器是发现竞态问题的利器,开发和测试时推荐使用
理解
并发安全的核心就是一句话:别让多个 goroutine 同时改同一个东西。简单的计数器用 atomic,复杂操作加 Mutex,初始化用 sync.Once。养成用 -race 检测的习惯,能在上线前把竞态 bug 揪出来。
引出
并发安全搞定了,Go 的基础语法和并发编程就基本学完了。但写程序不只是写代码,还要把代码组织好。接下来看 Go 包,学习怎么用包把代码按功能分组管理。