Files
cs-note/hzh/GolangStar/Go语言进阶/Sync.md
T

283 lines
6.8 KiB
Markdown
Raw Normal View History

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