11 同步模式

Go 同步模式详解

11.1 等待组 sync.WaitGroup 的原理是什么

基本概念

sync.WaitGroup 提供了用于创建等待多个并发执行的代码块在达到同步条件后,才可继续执行后续代码的能力。

基本原理

核心操作

  • Add(delta int):增加同步记录,相当于对运行计数执行 +delta 操作
  • Done():完成一个任务,相当于 Add(-1)
  • Wait():等待所有任务完成,阻塞直到运行计数变为 0
func (wg *WaitGroup) Done() {
    wg.Add(-1)
}

内部数据结构

type WaitGroup struct {
    state1 [3]uint32  // 运行计数、等待计数、信号计数
}

兼容性设计:由于 32 位系统无法对 64 位字段进行原子操作,使用 3 个 uint32 来分别计数。

state() 方法

func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
    if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
        // 32位系统:state1[0]和state1[1]用于计数,state1[2]用于信号
        return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
    }
    // 64位系统:state1[0]用于信号,state1[1]和state1[2]用于计数
    return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}

Wait 实现原理

func (wg *WaitGroup) Wait() {
    statep, semap := wg.state()
    for {
        state := atomic.LoadUint64(statep)
        v := int32(state >> 32)  // 运行计数
        if v == 0 {               // 运行计数为零,无须继续等待
            return
        }
        // 增加等待计数
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            // 休眠等待,信号计数减1
            runtime_Semacquire(semap)
            if *statep != 0 {
                panic("sync: WaitGroup is reused before previous Wait has returned")
            }
            return
        }
    }
}

关键点

  • 使用 runtime_Semacquire 让 goroutine 休眠,避免忙等待
  • 等待计数记录有多少个 goroutine 正在等待
  • 多个 goroutine 可以同时调用 Wait

Add 实现原理

func (wg *WaitGroup) Add(delta int) {
    statep, semap := wg.state()
    // 在运行计数上记录 delta
    state := atomic.AddUint64(statep, uint64(delta)<<32)
    v := int32(state >> 32)  // 运行计数值
    w := uint32(state)       // 等待计数值
    
    if v < 0 {
        panic("sync: negative WaitGroup counter")
    }
    if w != 0 && delta > 0 && v == int32(delta) {
        panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    if v > 0 || w == 0 {
        return
    }
    
    // 唤醒所有等待的 goroutine
    *statep = 0
    for ; w != 0; w-- {
        runtime_Semrelease(semap, false, 0)
    }
}

示例演示

wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
    go func() {
        time.Sleep(time.Millisecond * 100)
        wg.Done()
    }()
}
for i := 0; i < 10; i++ {
    go func(i int) {
        wg.Wait()
        println(i, ", wait done")
    }(i)
}
select {}

Happens Before 关系

WaitGroup 保证严格的 happens before 关系:

  1. 所有 Add 操作可分割为两个不交的组别 A、B,且 A happens before B
  2. 存在一个 Wait 操作 W,使得 A happens before W
  3. 且 W happens before B

这也是 WaitGroup 被称为"同步屏障"的原因。


11.2 缓存池 sync.Pool

概述

sync.Pool 是一个可以保存临时取还对象的"池子"。适合用于需要重复分配、回收内存的场景,可以减轻 GC 压力,提升系统性能。

注意:Pool 里装的对象可以无通知地被回收,可能 sync.Cache 是更合适的名字。

11.2.1 如何使用 sync.Pool

基本示例

var pool *sync.Pool

type Person struct {
    Name string
}

func initPool() {
    pool = &sync.Pool{
        New: func() interface{} {
            fmt.Println("Creating a new Person")
            return new(Person)
        },
    }
}

func main() {
    initPool()
    
    p := pool.Get().(*Person)
    fmt.Println("首次从 pool 里获取:", p)
    p.Name = "first"
    pool.Put(p)
    
    fmt.Println("Pool 里已有一个对象,Get: ", pool.Get().(*Person))
    fmt.Println("Pool 没有对象了,Get: ", pool.Get().(*Person))
}

输出

Creating a new Person
首次从 pool 里获取: &{}
Pool 里已有一个对象,Get:  &{first}
Creating a new Person
Pool 没有对象了,Get:  &{}

fmt 包中的使用

// src/fmt/print.go
var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

func (p *pp) free() {
    if cap(p.buf) > 64<<10 {
        return
    }
    p.buf = p.buf[:0]
    p.arg = nil
    p.value = reflect.Value{}
    p.wrappedErr = nil
    ppFree.Put(p)
}

gin 框架中的使用

engine.pool.New = func() interface{} {
    return engine.allocateContext()
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()
    engine.handleHTTPRequest(c)
    engine.pool.Put(c)
}

11.2.2 sync.Pool 是如何实现的

Pool 结构体

type Pool struct {
    noCopy noCopy
    
    local     unsafe.Pointer  // 每个 P 的本地队列,类型为 [P]poolLocal
    localSize uintptr         // local 数组的大小
    
    victim     unsafe.Pointer // 上一轮 GC 前的 local
    victimSize uintptr
    
    New func() interface{}    // 自定义的对象创建回调函数
}

poolLocal 结构体

type poolLocal struct {
    poolLocalInternal
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte  // 防止 false sharing
}

type poolLocalInternal struct {
    private interface{}  // P 的私有缓存区,使用时不需要加锁
    shared  poolChain    // 公共缓存区,其他 P 可 popTail
}

pad 字段作用:防止伪共享(false sharing)。将相关字段补齐到缓存行大小,避免多个 CPU 核心同时访问同一缓存行导致的性能问题。

poolChain 双端队列

type poolChain struct {
    head *poolChainElt  // 生产者操作
    tail *poolChainElt  // 消费者操作,需要原子控制
}

type poolChainElt struct {
    poolDequeue
    next, prev *poolChainElt
}

type poolDequeue struct {
    headTail uint64            // 包含 32 位 head 和 32 位 tail
    vals []eface               // 环形队列,size 必须是 2 的幂
}

特点

  • 单生产者、多消费者的固定大小无锁 Ring 队列
  • 生产者可以从 head 插入、head 删除
  • 消费者仅可从 tail 删除
  • 支持动态增长(通过双向链表)

Get 实现

func (p *Pool) Get() interface{} {
    l, pid := p.pin()          // 绑定当前 goroutine 和 P
    x := l.private
    l.private = nil
    if x == nil {
        x, _ = l.shared.popHead()      // 从本地 shared 头部取
        if x == nil {
            x = p.getSlow(pid)         // 从其他 P 偷取或从 victim 取
        }
    }
    runtime_procUnpin()                // 解除绑定
    
    if x == nil && p.New != nil {
        x = p.New()                    // 都没有则创建新对象
    }
    return x
}

pin 函数

func (p *Pool) pin() (*poolLocal, int) {
    pid := runtime_procPin()           // 禁止抢占
    s := atomic.LoadUintptr(&p.localSize)
    l := p.local
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid
    }
    return p.pinSlow()                 // 需要创建新的 poolLocal
}

procPin 实现

func procPin() int {
    _g_ := getg()
    mp := _g_.m
    mp.locks++          // locks > 0 表示不可被抢占
    return int(mp.p.ptr().id)
}

getSlow 偷取逻辑

func (p *Pool) getSlow(pid int) interface{} {
    size := atomic.LoadUintptr(&p.localSize)
    locals := p.local
    
    // 从其他 P 中偷取对象
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i+1)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }
    
    // 从 victim cache 中取对象
    size = atomic.LoadUintptr(&p.victimSize)
    if uintptr(pid) < size {
        locals = p.victim
        l := indexLocal(locals, pid)
        if x := l.private; x != nil {
            l.private = nil
            return x
        }
        for i := 0; i < int(size); i++ {
            l := indexLocal(locals, (pid+i)%int(size))
            if x, _ := l.shared.popTail(); x != nil {
                return x
            }
        }
    }
    
    atomic.StoreUintptr(&p.victimSize, 0)
    return nil
}

Put 实现

func (p *Pool) Put(x interface{}) {
    if x == nil {
        return
    }
    l, _ := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    if x != nil {
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
}

GC 清理机制

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

func poolCleanup() {
    // 清空旧的 victim
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }
    
    // 将 primary cache 移到 victim cache
    for _, p := range allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }
    
    oldPools, allPools = allPools, nil
}

Victim 机制的作用

  • 将 GC 的粒度放大,回收时间线拉长
  • 减少 GC 后冷启动导致的性能抖动
  • 原来 1 次 GC 的开销被拉长到 2 次且开销减小

Get 完整流程图

Get()
  │
  ├─ pin() 绑定当前 P
  │    │
  │    ├─ 取 l.private
  │    │
  │    ├─ 若为空,从 l.shared.popHead() 取
  │    │
  │    └─ 若仍为空,调用 getSlow()
  │         │
  │         ├─ 从其他 P 偷取 popTail()
  │         │
  │         └─ 从 victim 中取
  │
  ├─ runtime_procUnpin() 解绑
  │
  └─ 若仍为空且 New 不为空,调用 New() 创建

11.3 并发安全散列表 sync.Map

概述

sync.Map 是 Go 1.9 引入的并发安全的 map,具有以下特点:

  • 线程安全,读取、插入、删除都是常数级时间复杂度
  • 零值是有效的,零值是空 map
  • 第一次使用后不允许被复制

11.3.1 如何使用 sync.Map

func main() {
    var m sync.Map
    
    // 1. 写入
    m.Store("qcrao", 18)
    m.Store("stefno", 20)
    
    // 2. 读取
    age, _ := m.Load("qcrao")
    fmt.Println(age.(int))
    
    // 3. 遍历
    m.Range(func(key, value interface{}) bool {
        name := key.(string)
        age := value.(int)
        fmt.Println(name, age)
        return true
    })
    
    // 4. 删除
    m.Delete("qcrao")
    age, ok := m.Load("qcrao")
    fmt.Println(age, ok)
    
    // 5. 读取或写入
    m.LoadOrStore("stefno", 100)
    age, _ = m.Load("stefno")
    fmt.Println(age)
}

适用场景:读多写少。写多场景会导致性能下降。

11.3.2 sync.Map 底层实现

数据结构

type Map struct {
    mu     Mutex
    read   atomic.Value  // readOnly
    dirty  map[interface{}]*entry
    misses int
}

type readOnly struct {
    m       map[interface{}]*entry
    amended bool  // dirty map 中是否有 read 中没有的 key
}

type entry struct {
    p unsafe.Pointer  // 指向实际 value 的指针
}

entry 的三种状态

p 的三种状态:
┌─────────────────────────────────────────────┐
│ p == nil        │ 已删除,dirty 中无此 key │
├─────────────────────────────────────────────┤
│ p == expunged   │ 已删除,dirty 中有此 key │
├─────────────────────────────────────────────┤
│ p 指向正常值    │ 正常状态                  │
└─────────────────────────────────────────────┘
var expunged = unsafe.Pointer(new(interface{}))

整体结构图

sync.Map
├── mu (Mutex) ──── 保护 read 和 dirty
├── read (atomic.Value) ──── readOnly
│   ├── m: map[interface{}]*entry
│   └── amended: bool
├── dirty: map[interface{}]*entry
└── misses: int

Store 实现

func (m *Map) Store(key, value interface{}) {
    // fast path: 在 read 中找到且未被删除,直接 CAS 更新
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        // read 中存在
        if e.unexpungeLocked() {
            // 从 expunged 状态恢复,加入 dirty
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        // dirty 中存在
        e.storeLocked(&value)
    } else {
        // 都不存在
        if !read.amended {
            // 创建 dirty,从 read 复制未删除的 key
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

Load 实现

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            m.missLocked()  // 记录未命中
        }
        m.mu.Unlock()
    }
    
    if !ok {
        return nil, false
    }
    return e.load()
}

missLocked 与 dirty 提升

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    // 达到阈值,将 dirty 提升为 read
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

Delete 实现

func (m *Map) Delete(key interface{}) {
    m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            delete(m.dirty, key)
            m.missLocked()
        }
        m.mu.Unlock()
    }
    
    if ok {
        return e.delete()
    }
    return nil, false
}

Range 实现

func (m *Map) Range(f func(key, value interface{}) bool) {
    read, _ := m.read.Load().(readOnly)
    if read.amended {
        // 提升 dirty 为 read,将 O(n) 开销分摊
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if read.amended {
            read = readOnly{m: m.dirty}
            m.read.Store(read)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }
    
    for k, e := range read.m {
        v, ok := e.load()
        if !ok {
            continue
        }
        if !f(k, v) {
            break
        }
    }
}

总结

组件 核心思想 适用场景 关键特性
WaitGroup 计数器 + 信号量 等待一组 goroutine 完成 运行计数、等待计数、信号计数
sync.Pool 对象复用 + 分级缓存 频繁创建销毁的对象 每 P 独立缓存、victim 机制、无锁队列
sync.Map 读写分离 + 延迟删除 读多写少场景 read/dirty 双 map、entry 状态机、dirty 提升

sync.Pool 关键点

  • 伪共享防护:pad 字段补齐缓存行
  • Victim 机制:两级缓存,减轻 GC 压力
  • 无锁队列:poolDequeue 实现单生产者多消费者
  • 动态增长:poolChain 双向链表支持扩容

sync.Map 关键点

  • read 和 dirty 分离:read 无锁读,dirty 加锁写
  • entry 三种状态:nil、expunged、正常值
  • misses 计数:达到 dirty 长度时提升
  • 适用读多写少:写多会导致频繁提升,性能下降
posted @ 2026-03-30 15:32  cyusouyiku  阅读(2)  评论(0)    收藏  举报