2.3 并发同步与锁 #
虽然 Go 语言推崇"通过通信来共享内存"的并发模型,但在某些场景下,传统的同步原语仍然是必要的。Go 语言的 sync
包提供了一系列同步原语,用于保护共享资源、协调 Goroutine 执行和实现复杂的同步模式。
本节内容 #
学习目标 #
通过本节学习,您将能够:
- 理解各种同步原语的工作原理和适用场景
- 掌握 Mutex 和 RWMutex 的正确使用方法
- 学会使用 WaitGroup 协调多个 Goroutine
- 了解 Once 的单次执行保证机制
- 掌握 Cond 条件变量的使用技巧
- 能够选择合适的同步机制解决并发问题
- 避免常见的死锁和竞态条件
同步原语概述 #
何时使用同步原语 #
虽然 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 语言的同步原语!