283 lines
6.8 KiB
Markdown
283 lines
6.8 KiB
Markdown
---
|
||
tags: [go, golang, Sync, 并发安全, Mutex, WaitGroup]
|
||
create time: 2026-06-07 14:50
|
||
---
|
||
|
||
# sync 包
|
||
|
||
## 概述
|
||
|
||
当 channel 的"通信共享内存"哲学不完全适用时,`sync` 包提供了底层同步原语来保护共享资源的并发访问。本文覆盖 WaitGroup、Once、Mutex/RWMutex、Map、Atomic 和 Pool 六大组件的使用场景与最佳实践。
|
||
|
||
## 正文
|
||
|
||
### sync.WaitGroup — 等待一组 goroutine 完成
|
||
|
||
> [!question] 💭 思考
|
||
> 你启动了 10 个 goroutine 并行处理任务,怎么知道它们全部执行完毕?用 `time.Sleep`?这显然是不靠谱的。
|
||
|
||
```go
|
||
var wg sync.WaitGroup
|
||
wg.Add(3) // 等待 3 个 goroutine
|
||
|
||
go func() {
|
||
defer wg.Done() // 完成后计数器 -1
|
||
doTask1()
|
||
}()
|
||
go func() {
|
||
defer wg.Done()
|
||
doTask2()
|
||
}()
|
||
go func() {
|
||
defer wg.Done()
|
||
doTask3()
|
||
}()
|
||
|
||
wg.Wait() // 阻塞直到计数器归零
|
||
fmt.Println("all done")
|
||
```
|
||
|
||
> [!warning] ⚠️ 三个常见陷阱
|
||
> 1. **Add 必须在 goroutine 启动前调用**——否则可能漏计数
|
||
> 2. **Done 要用 defer**——确保即使 panic 也能正确递减
|
||
> 3. **计数器不能为负数**——否则会 panic
|
||
|
||
```go
|
||
// ❌ 危险:Add 在 goroutine 内,main 可能先执行 Wait
|
||
go func() {
|
||
wg.Add(1) // 太晚了!
|
||
defer wg.Done()
|
||
task()
|
||
}()
|
||
wg.Wait()
|
||
|
||
// ✅ 正确:Add 在启动前
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
task()
|
||
}()
|
||
wg.Wait()
|
||
```
|
||
|
||
### sync.Once — 只执行一次
|
||
|
||
> [!question] 💭 思考
|
||
> 配置文件加载、数据库连接初始化这类操作,如何保证在多线程环境下只执行一次且线程安全?
|
||
|
||
```go
|
||
var instance *Config
|
||
var once sync.Once
|
||
|
||
func GetConfig() *Config {
|
||
once.Do(func() {
|
||
instance = loadConfig() // 仅执行一次
|
||
})
|
||
return instance
|
||
}
|
||
```
|
||
|
||
| 对比项 | `init()` | `sync.Once` |
|
||
|--------|----------|-------------|
|
||
| 执行时机 | package 首次加载时 | 第一次调用 Do() 时 |
|
||
| 延迟性 | 不支持 | 支持懒加载 |
|
||
| 可控性 | 不可控 | 完全可控 |
|
||
|
||
> [!tip] 💡 使用场景
|
||
> - 单例模式初始化
|
||
> - 配置加载(懒加载避免启动开销)
|
||
> - 资源注册(如驱动注册)
|
||
|
||
### sync.Mutex / RWMutex — 互斥锁
|
||
|
||
#### 基础用法
|
||
|
||
> [!question] 💭 思考
|
||
> 多个 goroutine 同时对 `counter++` 操作,为什么结果总是不对?
|
||
|
||
`counter++` 不是原子操作,它等价于:读取 → 加 1 → 写回。两个 goroutine 可能同时读到相同的旧值,导致其中一次修改被覆盖。
|
||
|
||
```go
|
||
var (
|
||
counter int
|
||
mu sync.Mutex
|
||
)
|
||
|
||
func increment() {
|
||
mu.Lock()
|
||
counter++ // 临界区
|
||
mu.Unlock()
|
||
}
|
||
```
|
||
|
||
> [!tip] 💡 必记习惯:defer + Unlock
|
||
> ```go
|
||
> mu.Lock()
|
||
> defer mu.Unlock()
|
||
> // 临界区代码
|
||
> ```
|
||
> 这样可以确保任何退出路径(包括 return / panic)都能释放锁。
|
||
|
||
#### 读写锁 RWMutex
|
||
|
||
当读多写少时,RWMutex 可以显著提升并发度:
|
||
|
||
```go
|
||
var (
|
||
data map[string]string
|
||
rwmu sync.RWMutex
|
||
)
|
||
|
||
func Read(key string) (string, bool) {
|
||
rwmu.RLock()
|
||
defer rwmu.RUnlock() // 允许多个读并发
|
||
return data[key], true
|
||
}
|
||
|
||
func Write(key, val string) {
|
||
rwmu.Lock()
|
||
defer rwmu.Unlock() // 写时独占
|
||
data[key] = val
|
||
}
|
||
```
|
||
|
||
| 锁类型 | 读+读 | 读+写 | 写+写 | 说明 |
|
||
|--------|-------|-------|-------|------|
|
||
| Mutex | ❌ | ❌ | ❌ | 全部互斥 |
|
||
| RWMutex | ✅ | ❌ | ❌ | 读共享,写独占 |
|
||
|
||
> [!warning] ⚠️ 死锁:两种经典模式
|
||
|
||
**模式1:锁拷贝**
|
||
```go
|
||
func main() {
|
||
var mu sync.Mutex
|
||
mu.Lock()
|
||
copyMu := mu // ❌ 复制了锁的状态
|
||
// mu 仍持有锁,但 copyMu 是全新的未锁定锁
|
||
copyMu.Lock() // 这里永远不会返回(如果外层没 Unlock)
|
||
mu.Unlock()
|
||
}
|
||
```
|
||
|
||
**模式2:循环等待**
|
||
```go
|
||
go func() {
|
||
mu1.Lock(); defer mu1.Unlock()
|
||
time.Sleep(100 * time.Millisecond)
|
||
mu2.Lock(); defer mu2.Unlock() // 等 mu2
|
||
}()
|
||
|
||
go func() {
|
||
mu2.Lock(); defer mu2.Unlock()
|
||
time.Sleep(100 * time.Millisecond)
|
||
mu1.Lock(); defer mu1.Unlock() // 等 mu1 —— 死锁!
|
||
}()
|
||
```
|
||
|
||
> [!tip] 💡 避免死锁的规则
|
||
> 1. 始终通过指针传递 mutex
|
||
> 2. 统一锁的获取顺序(全局约定 mu1 < mu2 < mu3)
|
||
> 3. 尽量减少持锁时间,不要在临界区内做 IO 或网络调用
|
||
|
||
### sync.Map — 并发安全的 Map
|
||
|
||
Go 原生 `map` 不是并发安全的,并发读写会 panic:
|
||
|
||
```go
|
||
m := make(map[string]int)
|
||
go m["key"] = 1 // ❌ concurrent map writes: panic!
|
||
_ = m["key"] // ❌ concurrent map reads AND writes: panic!
|
||
```
|
||
|
||
解决方案有两种:
|
||
|
||
```go
|
||
// 方案1:map + mutex(适合频繁读写)
|
||
mu.Lock(); m["key"] = 1; mu.Unlock()
|
||
|
||
// 方案2:sync.Map(适合以下场景)
|
||
var sm sync.Map
|
||
sm.Store("key", 1)
|
||
v, ok := sm.Load("key")
|
||
```
|
||
|
||
> [!note] 📝 sync.Map 的适用场景
|
||
> - **读远多于写**:如缓存查找、路由表
|
||
> - **不相交的 key 集合**:每个 goroutine 只访问自己的 key
|
||
> - **不适合的场景**:频繁写入、需要遍历计数、单一 key 高频竞争
|
||
>
|
||
> 非并发场景下,普通 map + mutex 通常性能更好。
|
||
|
||
### sync/atomic — 原子操作
|
||
|
||
> [!question] 💭 思考
|
||
> 如果只是对一个整数做累加,用 Mutex 是不是太重了?
|
||
|
||
原子操作由 CPU 指令直接支持,无需操作系统介入,性能远高于 mutex:
|
||
|
||
```go
|
||
var counter int64
|
||
|
||
// 原子累加
|
||
atomic.AddInt64(&counter, 1)
|
||
|
||
// 原子读取
|
||
val := atomic.LoadInt64(&counter)
|
||
|
||
// 原子比较并交换
|
||
atomic.CompareAndSwapInt64(&counter, oldVal, newVal)
|
||
```
|
||
|
||
对于复合类型的原子操作,使用 `atomic.Value`:
|
||
|
||
```go
|
||
var config atomic.Value
|
||
|
||
// 初始化
|
||
config.Store(loadConfig())
|
||
|
||
// 读取(线程安全)
|
||
cfg := config.Load().(ConfigStruct)
|
||
```
|
||
|
||
> [!tip] 💡 Mutex vs Atomic 选择指南
|
||
> - **单个变量的简单操作**(累加、交换)→ atomic
|
||
> - **一段逻辑的互斥执行** → Mutex
|
||
> - **复杂结构体的原子更新** → atomic.Value
|
||
|
||
### sync.Pool — 对象复用池
|
||
|
||
```go
|
||
var pool = sync.Pool{
|
||
New: func() interface{} {
|
||
return &Buffer{data: make([]byte, 0, 1024)}
|
||
},
|
||
}
|
||
|
||
func useBuffer() {
|
||
buf := pool.Get().(*Buffer)
|
||
defer pool.Put(buf) // 用完放回
|
||
|
||
// 使用 buf...
|
||
|
||
// 如果有修改,放回前需 Reset
|
||
buf.Reset()
|
||
}
|
||
```
|
||
|
||
> [!warning] ⚠️ sync.Pool 的重要限制
|
||
> - Pool 中的对象**可能被 GC 随时回收**,不能依赖池中对象一定存在
|
||
> - 不适合存储有状态的对象(如 DB 连接、Socket)
|
||
> - Put 之前务必 Reset 对象状态,否则下次 Get 拿到的是脏数据
|
||
|
||
> [!tip] 💡 最佳实践
|
||
> 在高并发场景中,Pool 能显著降低 GC 压力——尤其是频繁创建销毁的大对象(如 byte slice、JSON encoder)。
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
|
||
- [[hzh/GolangStar/Go语言进阶/Channel]]
|
||
- [[hzh/GolangStar/Go语言进阶/Select]]
|
||
- [[hzh/GolangStar/Go语言进阶/协程池]]
|