Go-sync包并发原语详解从Mutex到Once的深度应用

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时触发,直接释放给队首等待者

自旋条件

  1. 运行在多核CPU上
  2. GOMAXPROCS > 1
  3. 至少存在一个正在运行的P
  4. 当前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的信号量模式

参考文献

  1. Go sync包文档: https://pkg.go.dev/sync
  2. Go内存模型 - The Go Memory Model
  3. Go源码 runtime/sema.go
  4. 《Go并发编程实战》- 汪明
  5. Bryan Mills - Rethinking Classical Concurrency Patterns
相关推荐
参.商.2 小时前
【Day 53】76. 最小覆盖子串
leetcode·golang
传奇开心果编程8 小时前
【Go入门练中学】第2课:条件与循环
后端·学习·golang
传奇开心果编程12 小时前
【Go入门练中学】第1课:从零开始
后端·学习·golang
报错小能手13 小时前
GO语言接口 泛型 错误处理
golang
蓝宝石的傻话2 天前
MiBee Eye 蜂眼:把闲置的 Linux 小板做成一台能进国标平台的摄像头
golang·rust
我不会起名字3222 天前
万字总结Golang入门项目
数据结构·经验分享·算法·golang·项目实战·复盘
我不会起名字3222 天前
一天一道算法题(34):回溯法的经典例题(子集)
java·数据结构·python·算法·golang·深度优先·力扣
A心有千千结2 天前
GO 使用 OpenTelemetry 进行编译时插桩,实现零码注入
数据库·golang·可观测性·观测云