Go sync包并发原语详解:从Mutex到Once的深度应用
文章导语
Go的并发哲学是"不要通过共享内存来通信,而要通过通信来共享内存"。但现实世界中,共享内存仍然是不可避免的。sync包提供了Mutex、RWMutex、WaitGroup、Once、Cond、Pool等一系列并发原语,掌握它们是写出正确并发程序的基石。
一、Mutex:互斥锁的底层原理
1.1 Mutex的三种模式
go
// runtime/sync/mutex.go
type Mutex struct {
state int32 // 锁状态
sema uint32 // 信号量
}
state字段包含四个信息:
- Waiter(29位):等待者的数量
- Starving(1位):是否处于饥饿模式
- Woken(1位):是否有goroutine被唤醒
- Locked(1位):是否已锁定
1.2 正常模式 vs 饥饿模式
go
// 正常模式:FIFO等待队列,新到达的goroutine尝试自旋获取锁
// 饥饿模式:当等待者等待超过1ms时触发,直接释放给队首等待者
自旋条件:
- 运行在多核CPU上
- GOMAXPROCS > 1
- 至少存在一个正在运行的P
- 当前P的本地运行队列为空
go
// 自旋的伪代码
for i := 0; i < spin; i++ {
for state&Locked == 0 {
if atomic.CAS(&m.state, state, state|Locked) {
return // 获取到锁
}
}
procyield(30) // CPU空转
}
二、RWMutex:读写锁的正确使用
go
// RWMutex的场景化选择
type Cache struct {
mu sync.RWMutex
items map[string]interface{}
}
// 读------用RLock
func (c *Cache) Get(key string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
return c.items[key]
}
// 写------用Lock
func (c *Cache) Set(key string, val interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = val
}
写优先机制:当有写锁等待时,新来的读锁需要排队等待,防止写锁饥饿。
三、WaitGroup:协程同步
go
// 经典使用模式
func processConcurrently(items []Item) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
item := item // Go 1.22前必须
go func() {
defer wg.Done()
process(item)
}()
}
wg.Wait()
}
// 常见错误:Add在goroutine内部
func wrong() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1) // 错误!可能在Wait()之后执行
defer wg.Done()
doWork()
}()
}
wg.Wait()
}
四、sync.Once:单次执行
go
// 线程安全的单例模式
type singleton struct{}
var (
instance *singleton
once sync.Once
)
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
内部实现:
go
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 { // 双重检查
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
五、sync.Pool:对象池
go
// 正确使用sync.Pool减少GC压力
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processRequest(data string) string {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.WriteString(data)
// 处理...
return buf.String()
}
// 注意:Pool中的对象可能被随时清理(GC时)
// 不要依赖Put()的对象一定能被Get()到
六、sync.Cond:条件变量
go
// 生产者-消费者模式(有限队列)
type Queue struct {
cond *sync.Cond
items []int
maxSize int
}
func (q *Queue) Put(item int) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.items) >= q.maxSize {
q.cond.Wait() // 等待消费者通知
}
q.items = append(q.items, item)
q.cond.Signal() // 通知等待的消费者
}
func (q *Queue) Get() int {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.items) == 0 {
q.cond.Wait()
}
item := q.items[0]
q.items = q.items[1:]
q.cond.Signal()
return item
}
七、全文总结
| 原语 | 使用场景 | 注意事项 |
|---|---|---|
| Mutex | 互斥访问共享资源 | 不可重入 |
| RWMutex | 读多写少 | 写优先 |
| WaitGroup | goroutine同步 | Add在go之前 |
| Once | 单次初始化 | 不可撤销 |
| Pool | 临时对象复用 | GC可能清理 |
| Cond | 条件等待 | 必须配合Lock |
八、技术进阶展望
- sync/atomic的低级原子操作
- errgroup的并发错误处理
- semaphore的信号量模式
参考文献
- Go sync包文档: https://pkg.go.dev/sync
- Go内存模型 - The Go Memory Model
- Go源码 runtime/sema.go
- 《Go并发编程实战》- 汪明
- Bryan Mills - Rethinking Classical Concurrency Patterns