线程安全
2026年9月6日
多个 goroutine 同时改同一块数据,结果可能对、也可能错,而且每次不一样。这就是竞态。调度是抢占的,++、-- 并不是原子操作。
两个 goroutine 一个加 100 次、一个减 100 次,理想结果是 0,不加保护时经常不是。
var n int
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
n++
}
}()
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
n--
}
}()
wg.Wait()
fmt.Println(n) // 不一定是 0
go
go run -race . 能把这种问题报出来。
同步锁
同一时刻只让一个 goroutine 进临界区,用 sync.Mutex:
var (
n int
mu sync.Mutex
wg sync.WaitGroup
)
func add(d int) {
defer wg.Done()
for i := 0; i < 1000; i++ {
mu.Lock()
n += d
mu.Unlock()
}
}
go
读多写少可以用 sync.RWMutex:RLock 允许多个读者,写还是独占。锁要成对,常用 defer mu.Unlock(),避免中途 return 忘了放。
只加减计数,也可以用 sync/atomic,比锁轻:
var n int64
atomic.AddInt64(&n, 1)
go
线程安全下的 map
内置 map 不是并发安全的。一边读一边写会直接报:
fatal error: concurrent map read and map write
text
见到这行,就是有多个 goroutine 在同时碰同一张 map。
两条路:
- 自己在读写外包一把锁
- 用
sync.Map
加锁:
var (
m = map[string]int{}
mu sync.Mutex
)
mu.Lock()
m["n"]++
mu.Unlock()
go
sync.Map 的 API 不同,没有下标语法:
var m sync.Map
m.Store("n", 1)
if v, ok := m.Load("n"); ok {
fmt.Println(v)
}
m.Range(func(k, v any) bool {
fmt.Println(k, v)
return true
})
go
它内部也用了锁和更细的结构,适合「写很少、读很多,key 比较稳定」的场景。一般业务 map 加一把 Mutex 就够。