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

6.8 KiB
Raw Blame History

tags, create time
tags create time
go
golang
Sync
并发安全
Mutex
WaitGroup
2026-06-07 14:50

sync 包

概述

当 channel 的"通信共享内存"哲学不完全适用时,sync 包提供了底层同步原语来保护共享资源的并发访问。本文覆盖 WaitGroup、Once、Mutex/RWMutex、Map、Atomic 和 Pool 六大组件的使用场景与最佳实践。

正文

sync.WaitGroup — 等待一组 goroutine 完成

[!question] 💭 思考 你启动了 10 个 goroutine 并行处理任务,怎么知道它们全部执行完毕?用 time.Sleep?这显然是不靠谱的。

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
// ❌ 危险: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] 💭 思考 配置文件加载、数据库连接初始化这类操作,如何保证在多线程环境下只执行一次且线程安全?

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 可能同时读到相同的旧值,导致其中一次修改被覆盖。

var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    counter++       // 临界区
    mu.Unlock()
}

[!tip] 💡 必记习惯:defer + Unlock

mu.Lock()
defer mu.Unlock()
// 临界区代码

这样可以确保任何退出路径(包括 return / panic)都能释放锁。

读写锁 RWMutex

当读多写少时,RWMutex 可以显著提升并发度:

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:锁拷贝

func main() {
    var mu sync.Mutex
    mu.Lock()
    copyMu := mu          // ❌ 复制了锁的状态
    // mu 仍持有锁,但 copyMu 是全新的未锁定锁
    copyMu.Lock()         // 这里永远不会返回(如果外层没 Unlock)
    mu.Unlock()
}

模式2:循环等待

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:

m := make(map[string]int)
go m["key"] = 1   // ❌ concurrent map writes: panic!
_ = m["key"]      // ❌ concurrent map reads AND writes: panic!

解决方案有两种:

// 方案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:

var counter int64

// 原子累加
atomic.AddInt64(&counter, 1)

// 原子读取
val := atomic.LoadInt64(&counter)

// 原子比较并交换
atomic.CompareAndSwapInt64(&counter, oldVal, newVal)

对于复合类型的原子操作,使用 atomic.Value:

var config atomic.Value

// 初始化
config.Store(loadConfig())

// 读取(线程安全)
cfg := config.Load().(ConfigStruct)

[!tip] 💡 Mutex vs Atomic 选择指南

  • 单个变量的简单操作(累加、交换)→ atomic
  • 一段逻辑的互斥执行 → Mutex
  • 复杂结构体的原子更新 → atomic.Value

sync.Pool — 对象复用池

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)。

关联笔记