--- 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语言进阶/协程池]]