Go 语言 sync 包使用教程
Go 语言 sync 包使用教程
Go 语言的 sync 包提供了基本的同步原语,用于在并发编程中协调 goroutine 之间的操作。
1. 互斥锁 (Mutex)
互斥锁用于保护共享资源,确保同一时间只有一个 goroutine 可以访问。
特点:
- 最基本的同步原语,实现互斥访问共享资源
- 有两个方法:
Lock()
和Unlock()
- 不可重入,同一个 goroutine 重复获取会导致死锁
- 没有超时机制,锁定后必须等待解锁
- 不区分读写操作,所有操作都是互斥的
- 适用于共享资源竞争不激烈的场景
- 性能高于 channel 实现的互斥机制
- 不保证公平性,可能导致饥饿问题
import (
"fmt"
"sync"
"time"
)
func main() {
var mutex sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
go func() {
mutex.Lock()
defer mutex.Unlock()
counter++
}()
}
time.Sleep(time.Second)
fmt.Println("计数器:", counter)
}
2. 读写锁 (RWMutex)
当多个 goroutine 需要读取而很少写入时,读写锁比互斥锁更高效。
特点:
- 针对读多写少场景优化的锁
- 提供四个方法:
RLock()
、RUnlock()
、Lock()
、Unlock()
- 允许多个读操作并发进行,但写操作是互斥的
- 写锁定时,所有读操作都会被阻塞
- 有读锁定时,写操作会等待所有读操作完成
- 写操作优先级较高,防止写饥饿
- 内部使用 Mutex 实现
- 比 Mutex 有更多开销,但在读多写少场景下性能更高
var rwMutex sync.RWMutex
var data map[string]string = make(map[string]string)
// 读取操作
func read(key string) string {
rwMutex.RLock()
defer rwMutex.RUnlock()
return data[key]
}
// 写入操作
func write(key, value string) {
rwMutex.Lock()
defer rwMutex.Unlock()
data[key] = value
}
3. 等待组 (WaitGroup)
等待组用于等待一组 goroutine 完成执行。
特点:
- 用于协调多个 goroutine 的完成
- 提供三个方法:
Add()
、Done()
、Wait()
Add()
增加计数器,参数可为负数Done()
等同于Add(-1)
,减少计数器Wait()
阻塞直到计数器归零- 计数器不能变为负数,会导致 panic
- 可以重用,计数器归零后可以再次增加
- 非常适合"扇出"模式(启动多个工作 goroutine 并等待全部完成)
- 不包含工作内容信息,仅表示完成状态
- 轻量级,开销很小
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 增加计数器
go func(id int) {
defer wg.Done() // 完成时减少计数器
fmt.Printf("工作 %d 完成\n", id)
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("所有工作已完成")
}
4. 一次性执行 (Once)
Once 确保一个函数只执行一次,无论有多少 goroutine 尝试执行它。
特点:
- 确保某个函数只执行一次
- 只有一个方法:
Do(func())
- 即使在多个 goroutine 中调用也只执行一次
- 常用于单例模式或一次性初始化
- 内部使用互斥锁和一个标志位实现
- 非常轻量级,几乎没有性能开销
- 如果传入的函数 panic,视为已执行
- 不能重置,一旦执行就不能再次执行
- 传入不同的函数也不会再次执行
var once sync.Once
var instance *singleton
func getInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
5. 条件变量 (Cond)
条件变量用于等待或宣布事件的发生。
特点:
- 用于等待或通知事件发生
- 需要与互斥锁结合使用:
sync.NewCond(&mutex)
- 提供三个方法:
Wait()
、Signal()
、Broadcast()
Wait()
自动解锁并阻塞,被唤醒后自动重新获取锁Signal()
唤醒一个等待的 goroutineBroadcast()
唤醒所有等待的 goroutine- 适合生产者-消费者模式
- 可以避免轮询,提高性能
- 使用相对复杂,容易出错
- 等待必须在获取锁后调用
var mutex sync.Mutex
var cond = sync.NewCond(&mutex)
var ready bool
func main() {
go producer()
// 消费者
mutex.Lock()
for !ready {
cond.Wait() // 等待条件变为真
}
fmt.Println("数据已准备好")
mutex.Unlock()
}
func producer() {
time.Sleep(time.Second) // 模拟工作
mutex.Lock()
ready = true
cond.Signal() // 通知一个等待的 goroutine
// 或使用 cond.Broadcast() 通知所有等待的 goroutine
mutex.Unlock()
}
6. 原子操作 (atomic)
对于简单的计数器或标志,可以使用原子操作包而不是互斥锁。
特点:
- 底层的原子操作,无锁实现
- 适用于简单的计数器或标志位
- 比互斥锁性能更高,开销更小
- 提供多种原子操作:
Add
、Load
、Store
、Swap
、CompareAndSwap
- 支持多种数据类型:int32、int64、uint32、uint64、uintptr 和指针
- 可用于实现自己的同步原语
- 不适合复杂的共享状态
- 在多 CPU 系统上可能导致缓存一致性开销
- Go 1.19 引入了新的原子类型
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var counter int64 = 0
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
time.Sleep(time.Second)
fmt.Println("计数器:", atomic.LoadInt64(&counter))
}
7. Map (sync.Map)
Go 1.9 引入的线程安全的 map。
特点:
- Go 1.9 引入的线程安全的哈希表
- 无需额外加锁即可安全地并发读写
- 提供五个方法:
Store
、Load
、LoadOrStore
、Delete
、Range
- 内部使用分段锁和原子操作优化性能
- 适用于读多写少的场景
- 不保证遍历的顺序
- 不支持获取元素数量或判断是否为空
- 不能像普通 map 那样直接使用下标语法
- 性能比加锁的普通 map 更好,但单线程下比普通 map 慢
- 内存开销较大
var m sync.Map
func main() {
// 存储键值对
m.Store("key1", "value1")
m.Store("key2", "value2")
// 获取值
value, ok := m.Load("key1")
if ok {
fmt.Println("找到键:", value)
}
// 如果键不存在则存储
m.LoadOrStore("key3", "value3")
// 删除键
m.Delete("key2")
// 遍历所有键值对
m.Range(func(key, value interface{}) bool {
fmt.Println(key, ":", value)
return true // 返回 false 停止遍历
})
}
8. Pool (sync.Pool)
对象池用于重用临时对象,减少垃圾回收压力。
特点:
- 用于缓存临时对象,减少垃圾回收压力
- 提供两个方法:
Get()
和Put()
- 需要提供
New
函数来创建新对象 - 对象可能在任何时候被垃圾回收,不保证存活
- 在 GC 发生时会清空池中的所有对象
- 不适合管理需要显式关闭的资源(如文件句柄)
- 适合于频繁创建和销毁的对象
- 没有大小限制,Put 总是成功的
- 每个 P(处理器)有自己的本地池,减少竞争
- Go 1.13 后大幅提升了性能
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process() {
// 获取缓冲区
buffer := bufferPool.Get().(*bytes.Buffer)
buffer.Reset() // 清空以便重用
// 使用缓冲区
buffer.WriteString("hello")
// 操作完成后放回池中
bufferPool.Put(buffer)
}
9. 综合示例
下面是一个综合示例,展示了多个同步原语的使用:
package main
import (
"fmt"
"sync"
"time"
)
type SafeCounter struct {
mu sync.Mutex
wg sync.WaitGroup
count int
}
func main() {
counter := SafeCounter{}
// 启动 5 个 goroutine 增加计数器
for i := 0; i < 5; i++ {
counter.wg.Add(1)
go func(id int) {
defer counter.wg.Done()
for j := 0; j < 10; j++ {
counter.mu.Lock()
counter.count++
fmt.Printf("Goroutine %d: 计数器 = %d\n", id, counter.count)
counter.mu.Unlock()
// 模拟工作
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 等待所有 goroutine 完成
counter.wg.Wait()
fmt.Println("最终计数:", counter.count)
}
最佳实践
-
使用 defer 解锁:确保即使发生错误也能解锁
mu.Lock() defer mu.Unlock()
-
避免锁的嵌套:容易导致死锁
-
保持临界区简短:锁定时间越短越好
-
基准测试比较:
- 对于大多数简单操作,atomic 比 Mutex 快
- RWMutex 在读操作远多于写操作时优于 Mutex
- sync.Map 在高并发下比加锁的 map 性能更好
-
内存对齐:
- 原子操作需要内存对齐
- 不正确的内存对齐会严重影响性能
- 特别是在 32 位系统上使用 64 位原子操作
-
超时控制:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() done := make(chan struct{}) go func() { // 执行可能耗时的操作 mu.Lock() // ... mu.Unlock() done <- struct{}{} }() select { case <-done: // 操作成功完成 case <-ctx.Done(): // 操作超时 }