深入理解Go语言的sync包 – 并发同步原语指南
1. sync包概述
Go语言的sync
包提供了基本的同步原语,用于在并发程序中协调不同的goroutine。在多线程编程中,正确使用同步机制至关重要,可以避免数据竞争和确保程序行为的确定性。
2. 互斥锁 Mutex
2.1 基本用法
sync.Mutex
是最简单的同步原语,保证同一时间只有一个goroutine能访问共享资源:
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
2.2 使用建议
- 锁定的区域应尽可能小
- 确保所有可能的分支都会解锁
- 使用
defer mu.Unlock()
可以避免忘记解锁
func safeWrite(m map[string]string, key, value string) {
mu.Lock()
defer mu.Unlock()
m[key] = value
}
3. 读写锁 RWMutex
3.1 基本概念
sync.RWMutex
允许多个读者或一个写者:
var (
cache map[string]string
rw sync.RWMutex
)
func read(key string) string {
rw.RLock()
defer rw.RUnlock()
return cache[key]
}
func write(key, value string) {
rw.Lock()
defer rw.Unlock()
cache[key] = value
}
3.2 适用场景
- 读多写少的场景
- 数据读取频率远高于写入频率
- 允许一定程度的一致性妥协
4. 等待组 WaitGroup
4.1 使用示例
sync.WaitGroup
用于等待一组goroutine完成:
func process(data []string) {
var wg sync.WaitGroup
for _, item := range data {
wg.Add(1)
go func(item string) {
defer wg.Done()
// 处理item
}(item)
}
wg.Wait() // 等待所有goroutine完成
}
4.2 最佳实践
Add()
应在gouroutine外调用- 使用
defer wg.Done()
确保计数减少 - 不要将WaitGroup传递给gouroutine,应传递指针
5. 一次性初始化 Once
5.1 原理与用法
sync.Once
确保一个函数只执行一次:
var (
config *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
config = loadConfig()
})
return config
}
5.2 使用场景
- 懒加载
- 单例模式实现
- 全局初始化
6. 条件变量 Cond
6.1 基本使用
sync.Cond
用于goroutine间的通知:
var (
mu sync.Mutex
cv = sync.NewCond(&mu)
val int
)
func waitForValue() {
mu.Lock()
for val == 0 {
cv.Wait() // 等待通知
}
fmt.Println("Got value:", val)
mu.Unlock()
}
func setValue(v int) {
mu.Lock()
val = v
mu.Unlock()
cv.Broadcast() // 通知所有等待者
}
6.2 注意事项
- 总是持有锁时调用
Wait()
- 检查条件应放在循环中
- 使用
Broadcast()
或Signal()
唤醒等待者
7. 并发安全的Map
7.1 sync.Map特性
sync.Map
是线程安全的map实现:
var m sync.Map
// 存储值
m.Store("key", "value")
// 加载值
if v, ok := m.Load("key"); ok {
fmt.Println(v)
}
// 遍历所有键值
m.Range(func(k, v interface{}) bool {
fmt.Println(k, v)
return true // 继续遍历
})
7.2 适用场景
- 读多写少
- 键集合相对稳定
- 每个键有独立的更新
8. 常见陷阱与最佳实践
8.1 避免死锁
- 按固定顺序获取多个锁
- 使用
defer
释放锁 - 设置超时机制
8.2 性能考量
- RWMutex不一定比Mutex快
sync.Map
不是所有场景都比map+mutex
更好- 监控锁竞争情况
8.3 错误处理
- 使用
TryLock
(Go 1.18+)避免阻塞 - 考虑上下文取消
- 记录锁持有时间过长的场景
文末预告
下一期我们将深入探讨Go语言的context
包,了解如何优雅地管理goroutine的生命周期,实现请求作用域的值传递、取消信号和超时控制等功能。