Skip to content
Go
sync — 同步原语

sync — 同步原语

sync 提供了 Go 并发编程的基础构件。理解它们的底层语义和边界条件是写出正确并发代码的前提。


1. Mutex — 互斥锁

1.1 基础用法

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()  // defer 保证即使 panic 也解锁
    c.value++
}

🔬 深入原理sync.Mutex 在 Go 1.18+ 添加了 TryLock() 方法。正常模式(非饥饿模式)下,等待的 goroutine 以 FIFO 队列等待,但新到达的 goroutine 有优势(它们可能自旋并直接获取锁)。如果一个等待者超过 1ms 仍未获取,锁进入饥饿模式,使用 FIFO 直接交接。

1.2 🚨 不可复制

type SafeStruct struct {
    mu    sync.Mutex // 陷阱:包含 Mutex 的结构体不能被值复制
    data  map[string]string
}

// 错误做法
func (s SafeStruct) BadMethod() {  // 值接收者 → 复制了 Mutex
    s.mu.Lock()
    defer s.mu.Unlock()
}

// 正确做法
func (s *SafeStruct) GoodMethod() {  // 指针接收者
    s.mu.Lock()
    defer s.mu.Unlock()
}

🚨 顶级陷阱:一旦 Mutex 被复制,副本和原始值各自独立,起不到保护作用。用 go vet 可以检测这类问题。

1.3 锁的粒度

// ❌ 锁粒度过大 — 把不相关的操作放在锁里
func (c *Counter) BadUpdate() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++         // 需要保护
    log.Printf(...)    // 不需要保护,但也被锁了
    metrics.Report()   // 可能造成死锁(如果 metrics 也获取这个锁)
}

// ✅ 正确的粒度
func (c *Counter) GoodUpdate() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
    // 锁外操作
    log.Printf(...)
    metrics.Report()
}

2. RWMutex — 读写锁

💡 核心原则:读共享,写独占。多读不互斥,有写时禁止读。

2.1 基础用法

type SafeMap struct {
    mu   sync.RWMutex
    data map[string]string
}

func (m *SafeMap) Get(key string) (string, bool) {
    m.mu.RLock()         // 读锁(可以多个并发)
    defer m.mu.RUnlock()
    val, ok := m.data[key]
    return val, ok
}

func (m *SafeMap) Set(key, value string) {
    m.mu.Lock()          // 写锁(独占)
    defer m.mu.Unlock()
    m.data[key] = value
}

2.2 🔬 深入原理:读锁不能升级为写锁

// 以下代码造成死锁:
func (m *SafeMap) DeadlockedUpdate(key string, fn func(string) string) {
    m.mu.RLock()
    old := m.data[key]
    // 注意:这里试图在持有读锁时获取写锁
    m.mu.Lock() // ← 死锁!RWMutex 禁止读锁升级
    m.data[key] = fn(old)
    m.mu.Unlock()
    m.mu.RUnlock()
}

// 正确做法:先释放读锁再获取写锁
func (m *SafeMap) CorrectUpdate(key string, fn func(string) string) {
    m.mu.RLock()
    old, exists := m.data[key]
    m.mu.RUnlock()

    if !exists {
        return
    }

    m.mu.Lock()
    m.data[key] = fn(old)
    m.mu.Unlock()
}

🔬 **为什么 RWMutex 不允许升级?**因为如果允许多个等待升级的 goroutine 同时存在,就没有一个明确的调度方案。RWMutex 选择了简单的语义:Read Lock → Write Lock = 死锁。

2.3 何时用 RWMutex 而不是 Mutex?

  • 读多写少 → RWMutex(典型比例 > 10:1 才有明显收益)。
  • 写多读少 → Mutex(RWMutex 的内部开销更大,反而更慢)。
  • 不确定 → 用 Mutex,简单且不容易出错。

3. WaitGroup — 等待组

3.1 基础用法

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)          // 必须在 goroutine 外调用
    go func(id int) {
        defer wg.Done()   // = Add(-1)
        doWork(id)
    }(i)
}

wg.Wait()  // 阻塞直到计数器归零

🚨 陷阱wg.Add(1) 必须在 goroutine 启动之前调用。如果放在 goroutine 里面,可能在 wg.Wait() 已经返回之后才执行,导致 panic(在计数归零之后不能再 Add 负数)。

3.2 WaitGroup 不能复制

func bad(wg sync.WaitGroup) { // 值传递 — 复制了计数器!
    wg.Done()
}

func good(wg *sync.WaitGroup) {
    wg.Done()
}

4. Once — 一次性执行

4.1 经典用法:单例模式

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{data: "init"}
    })
    return instance
}

4.2 🔬 深入原理:Once 的原子性保证

sync.Once 保证:

  1. Do 中的函数恰好执行一次
  2. 在函数返回之前,所有 Do 调用者都会阻塞。
  3. 如果函数 panic,Once 认为它已经执行过了,不会重试。
// Once 的实现原理(简化版)
type Once struct {
    done uint32 // 原子标志
    m    Mutex
}

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {  // double-check
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

🚨 陷阱:如果 f() 内部调用了同一个 Once.Do(f2),形成递归调用 — 由于 Mutex 不可重入,这会造成死锁。


5. Cond — 条件变量

Cond 是 sync 中最容易用错的类型,但也是最强大的。它实现了"等待某个条件成立"的模式。

5.1 为什么需要 Cond?

// ❌ 忙等 — 浪费 CPU
for !condition() {
    time.Sleep(10 * time.Millisecond)
}

// ❌ channel — 但需要反复通知的场景难以表达
// 如果有多个 goroutine 都在等待"队列非空"

// ✅ Cond — 高效等待 + 广播唤醒

5.2 🔬 深入原理:Cond 的正确使用范式

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu)    // ← Cond 绑定一个 Locker
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    q.items = append(q.items, item)
    q.mu.Unlock()
    q.cond.Signal()  // 唤醒一个等待者(可以在锁外调用)
}

func (q *Queue) Get() int {
    q.mu.Lock()
    defer q.mu.Unlock()

    // 关键模式:用 for 而不是 if!
    for len(q.items) == 0 {
        q.cond.Wait()  // 原子地:释放锁 + 阻塞 + 被唤醒后重新获取锁
    }

    item := q.items[0]
    q.items = q.items[1:]
    return item
}

🚨 三个关键点

  1. Wait 必须在锁内调用 — 因为条件检查需要锁保护。
  2. 用 for 而不是 if — 被唤醒不等于条件成立(虚假唤醒)。
  3. Signal 可以在锁外调用 — 减少锁竞争。

5.3 Signal vs Broadcast

// Signal — 唤醒一个等待的 goroutine
q.cond.Signal()

// Broadcast — 唤醒所有等待的 goroutine
q.cond.Broadcast()

💡 最佳实践

  • 状态变化只影响一个等待者 → Signal
  • 状态变化使所有等待者都有机会 → Broadcast(如连接池关闭、全局状态切换)。
  • 不确定时 → 用 Broadcast(更安全,不会有 goroutine 被永久忽略)。

6. Pool — 对象池

复用临时对象,减少 GC 压力。但 Pool 的行为不是你能完全控制的

6.1 基础用法

var bufferPool = sync.Pool{
    New: func() interface{} {
        return &bytes.Buffer{}
    },
}

// 获取
buf := bufferPool.Get().(*bytes.Buffer)

// 使用
buf.WriteString("hello")
result := buf.String()

// 归还前重置
buf.Reset()
bufferPool.Put(buf)

6.2 🔬 深入原理:Pool 的关键特性

  1. Pool 中的对象可能随时被 GC 清除 — 你不能假设 Put 进去的对象一定还在。
  2. 不要对 Pool.Get 返回值的状态做任何假设 — 始终在使用前重置。
  3. Pool 是 goroutine 安全的,但不是为长期持有优化的。
  4. Pool 的大小会随着 GC 动态缩减 — 每次 GC 会清空 Pool。
// ❌ 错误的用法 — 假设对象被保留
var pool sync.Pool
func init() {
    pool.Put(expensiveSetup()) // 没用!可能随时被 GC 清掉
}

// ✅ 正确的用法 — 用 New 函数懒加载
var pool = sync.Pool{
    New: func() interface{} {
        return expensiveSetup()
    },
}

💡 Pool 适用场景

  • 频繁创建和销毁的临时对象(buffer、encoder/decoder)。
  • 对象创建成本高,但可以无状态地复用。

不适用场景

  • 需要维护固定数量的长连接(用 channel pool)。
  • 对象有状态或需要初始化(Pool.Get 返回的状态是未知的)。

7. TryLock (Go 1.18+)

var mu sync.Mutex

if mu.TryLock() {
    defer mu.Unlock()
    // 获取到锁
} else {
    // 没获取到,做其他事
}

💡 最佳实践TryLock 主要用于死锁避免(如需要同时获取多个锁时),而不是作为通用的"非阻塞版本"。大多数情况下应该用普通的 Lock


8. 🚨 sync 包常见陷阱总汇

陷阱 后果 检测方法
复制 Mutex/WaitGroup 锁失效/panic go vet
读锁内获取写锁 死锁 -race
wg.Add 放在 goroutine 内 计数错误/panic -race
Once.Do 内调用自己 死锁 运行时阻塞
Pool 依赖 Put 的对象一直存在 逻辑错误 仅在 GC 后出现
defer mu.Unlock() 前检查 TryLock 返回值 Unlock 未持有的锁 review