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 关系:
- 所有 Add 操作可分割为两个不交的组别 A、B,且 A happens before B
- 存在一个 Wait 操作 W,使得 A happens before W
- 且 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 长度时提升
- 适用读多写少:写多会导致频繁提升,性能下降

浙公网安备 33010602011771号