2.3 并发同步与锁

2.3 并发同步与锁 #

虽然 Go 语言推崇"通过通信来共享内存"的并发模型,但在某些场景下,传统的同步原语仍然是必要的。Go 语言的 sync 包提供了一系列同步原语,用于保护共享资源、协调 Goroutine 执行和实现复杂的同步模式。

本节内容 #

学习目标 #

通过本节学习,您将能够:

  1. 理解各种同步原语的工作原理和适用场景
  2. 掌握 Mutex 和 RWMutex 的正确使用方法
  3. 学会使用 WaitGroup 协调多个 Goroutine
  4. 了解 Once 的单次执行保证机制
  5. 掌握 Cond 条件变量的使用技巧
  6. 能够选择合适的同步机制解决并发问题
  7. 避免常见的死锁和竞态条件

同步原语概述 #

何时使用同步原语 #

虽然 Channel 是 Go 语言推荐的并发通信方式,但在以下场景中,传统的同步原语可能更合适:

  • 保护共享状态:多个 Goroutine 需要访问同一个数据结构
  • 性能敏感场景:需要最小化同步开销
  • 复杂同步逻辑:需要实现复杂的等待和通知机制
  • 与外部库集成:需要与使用传统同步机制的代码集成

sync 包提供的同步原语 #

  • Mutex:互斥锁,提供排他性访问
  • RWMutex:读写锁,允许多个读者或一个写者
  • WaitGroup:等待组,等待多个 Goroutine 完成
  • Once:单次执行,确保函数只执行一次
  • Cond:条件变量,实现等待和通知机制

性能考虑 #

不同的同步原语有不同的性能特征:

  • Channel:适合通信和数据传递,有一定的开销
  • Mutex:轻量级,适合简单的互斥访问
  • RWMutex:适合读多写少的场景
  • 原子操作:最轻量级,适合简单的数值操作

设计原则 #

1. 最小化锁的范围 #

// 好的做法:锁的范围最小
func (c *Counter) Increment() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

// 不好的做法:锁的范围过大
func (c *Counter) BadIncrement() {
    c.mu.Lock()
    defer c.mu.Unlock()

    // 大量不需要同步的操作
    time.Sleep(time.Millisecond)
    log.Println("Incrementing...")
    c.value++
}

2. 避免嵌套锁 #

// 容易导致死锁的嵌套锁
type Account struct {
    mu      sync.Mutex
    balance int
}

func Transfer(from, to *Account, amount int) {
    from.mu.Lock()
    defer from.mu.Unlock()

    to.mu.Lock()    // 可能导致死锁
    defer to.mu.Unlock()

    from.balance -= amount
    to.balance += amount
}

// 更好的做法:按固定顺序获取锁
func SafeTransfer(from, to *Account, amount int) {
    if from == to {
        return
    }

    // 按内存地址排序,避免死锁
    first, second := from, to
    if uintptr(unsafe.Pointer(from)) > uintptr(unsafe.Pointer(to)) {
        first, second = to, from
    }

    first.mu.Lock()
    defer first.mu.Unlock()

    second.mu.Lock()
    defer second.mu.Unlock()

    from.balance -= amount
    to.balance += amount
}

3. 使用 defer 确保解锁 #

func (c *Counter) GetValue() int {
    c.mu.Lock()
    defer c.mu.Unlock()  // 确保在函数返回前解锁

    if c.value < 0 {
        return 0
    }
    return c.value
}

前置知识 #

在学习本节内容之前,您需要掌握:

  • Goroutine 的基本概念和使用
  • Channel 的基本操作
  • Go 语言的内存模型基础
  • 并发编程中的竞态条件概念

让我们开始深入学习 Go 语言的同步原语!