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

283 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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语言进阶/协程池]]