Appearance
32|sync 包与锁
多个 goroutine 同时读写同一个变量时,需要同步保护。普通 map 并发写会触发 fatal error: concurrent map writes,多个 goroutine 同时自增计数器会得到错误的结果。sync 包提供了等待、加锁和单次执行等原语,是 Go 并发编程的基础工具。
一、sync.WaitGroup
sync.WaitGroup 用来等待一组 goroutine 全部结束。它只负责等待,不负责传数据:
go
package main
import (
"fmt"
"sync"
"time"
)
func check(name string) {
time.Sleep(1 * time.Second)
fmt.Println(name, "ok")
}
func main() {
hosts := []string{"web01", "web02", "db01"}
var wg sync.WaitGroup
for _, host := range hosts {
wg.Add(1)
go func(name string) {
defer wg.Done()
check(name)
}(host)
}
wg.Wait()
fmt.Println("all checks finished")
}WaitGroup 内部可以近似看成一个计数器。Add(1) 表示多一个任务要等,Done() 表示一个任务结束,Wait() 会一直等到计数归零。
Done() 少调用一次,程序会一直卡在 Wait();Done() 调多了,计数变成负数会触发 panic。循环里启动 goroutine 时,把循环变量作为参数传进去更稳——旧代码里如果直接在闭包里使用外层循环变量,容易出现所有任务读到同一个值的问题。
二、sync.Mutex
多个 goroutine 同时读写同一个变量时,需要同步保护:
go
package main
import (
"fmt"
"sync"
)
func main() {
counts := map[string]int{}
var mu sync.Mutex
var wg sync.WaitGroup
for _, service := range []string{"nginx", "nginx", "mysql"} {
wg.Add(1)
go func(name string) {
defer wg.Done()
mu.Lock()
counts[name]++
mu.Unlock()
}(service)
}
wg.Wait()
fmt.Println(counts)
}Mutex 是互斥锁,同一时间只有一个 goroutine 能持有锁。Lock 和 Unlock 之间的代码叫临界区,应该尽量短——锁住的代码越长,并发性能越差。
defer mu.Unlock() 比手动 Unlock 更安全,避免中间提前 return 时忘记释放锁:
go mu.Lock()
defer mu.Unlock()
counts[name]++三、sync.RWMutex
读多写少时用 sync.RWMutex,它区分读锁和写锁。多个 goroutine 可以同时持有读锁,但写锁独占:
go
var cache = make(map[string]string)
var rwmu sync.RWMutex
func get(key string) string {
rwmu.RLock()
defer rwmu.RUnlock()
return cache[key]
}
func set(key, value string) {
rwmu.Lock()
defer rwmu.Unlock()
cache[key] = value
}读操作远多于写操作时,RWMutex 的并发性能比 Mutex 好得多。但如果写操作很频繁,读锁不断被写锁阻塞,性能反而不如 Mutex。
数据能通过 channel 汇总时,通常比共享 map 再加锁更清楚。channel 把状态流转显式化,而锁隐藏了状态变更的时机。需要频繁读写公共状态时,再考虑 Mutex。
四、sync.Once
sync.Once 保证某个操作只执行一次,常用于初始化:
go
var once sync.Once
var config *Config
func GetConfig() *Config {
once.Do(func() {
config = loadConfig()
})
return config
}无论 GetConfig 被多少 goroutine 同时调用,loadConfig 只执行一次。sync.Once 比手动用 Mutex + bool 标志更简洁,且内部处理了并发竞争。
五、sync.Map
Go 1.9 引入的 sync.Map 是并发安全的 map,但 API 和普通 map 不同,不适合所有场景。它适合两种场景:
- 键值对只写一次、读很多次(如缓存)
- 多个 goroutine 读写不同的 key,互不重叠
go
var m sync.Map
m.Store("key", "value")
val, ok := m.Load("key")
m.Delete("key")
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // 继续遍历
})普通 map 用 Mutex 保护更直观,sync.Map 只在特定场景下性能更好。
六、常见错误
WaitGroup 计数不匹配
go
wg.Add(1)
go func() {
// 忘了调用 wg.Done()
}()
wg.Wait() // 永远阻塞defer wg.Done() 在 goroutine 开头注册,能避免所有退出路径遗漏。
复制 sync 类型
go
func process(mu sync.Mutex) { // 错误:复制了 Mutex
mu.Lock()
defer mu.Unlock()
}sync.Mutex、sync.WaitGroup 等类型不应该被复制。函数参数应该传指针:func process(mu *sync.Mutex)。
锁的粒度太粗
go
mu.Lock()
result := heavyComputation()
data[name] = result
mu.Unlock()heavyComputation 不需要锁保护,把它放到锁外面,临界区只保护 map 写入:
go
result := heavyComputation()
mu.Lock()
data[name] = result
mu.Unlock()死锁
go
mu1.Lock()
mu2.Lock() // 如果另一个 goroutine 先锁了 mu2 再请求 mu1,死锁多个锁的获取顺序不一致会导致死锁。需要多个锁时, always 按固定顺序获取。