线程安全

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.RWMutexRLock 允许多个读者,写还是独占。锁要成对,常用 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。

两条路:

  1. 自己在读写外包一把锁
  2. 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 就够。


参考文档