1.原子操作(sync.atomic包)
sync.atomic
包提供低级别的内存原子操作。除非特殊情况,建议还是通过channel
或者sync
包实现同步。
交换(swap
)操作由SwapT
函数实现,等同于:
1 | old = *addr |
CAS(compare-and-swap
)操作由CompareAndSwapT
系列函数实现, 等同于:
1 | if *addr == old { |
add
操作由AddT
系列函数实现, 等同于:
1 | *addr += delta |
load
和store
操作对应LoadT
和StoreT
系列函数,等同于return *addr
和*addr = val
。
在golang内存模型中,如果原子操作A的影响会被原子操作B观察到,那么Asynchronizes before
B。并且,一个程序中的所有原子操作表现的像是按一定程度的顺序一致性。这个定义和C++的顺序一致原子操作,以及Java里面的volatile一致。
实现上也和Java的老版Atomic*类一致,CAS操作在x86上对应的是cmpxchg
指令。
2.同步原语(sync包)
sync
包为golang提供了基本的同步原语(synchronization primitive),不过包里面除了Once
和WaitGroup
,大部份都是为了低级别库使用。高级同步,golang文档建议是采用channel
和通信。
sync
包中的基本原语,包括Mutex
、RWMutex
、WaitGroup
、Pool
、Map
、Once
和Cond
。
Mutex
Mutex
不是公平锁,存在正常模式和饥饿模式两种模式。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免goroutine由于陷入等待无法获取锁而造成的高尾延时。
背景,在初始版本中通过cas
和信号量实现,后续版本中出于性能优化,使得新goroutine获取锁的几率大增,导致旧goroutine无法竞争过新的,从而发生饥饿情况。原因是正常模式下,waiter
(goroutine)虽然是按FIFO的顺序入队,但是刚唤醒的goroutine不会直接获取到锁,而是去竞争。在这种情况下,与之竞争的新goroutine,会因为已经被CPU执行,更容易获取到锁。
在饥饿模式下,mutex的所有权会直接从解锁的goroutine转交给队首的waiter。新到的goroutine不立刻竞争锁,也不自旋,而是排到队尾。
1 | type Mutex struct { |
state
最低三位分别表示mutexLocked(1)、mutexWoken(2)和mutexStarving(4),剩下的位置表示当前有多少个goroutine在等待互斥锁的释放。
加锁过程:优先CAS,然后通过自旋+信号量获取锁。
解锁过程:饥饿模式直接将所有权交与下一个waiter,普通模式直接返回,或者通过信号量唤醒其他waiter。
RWMutex
1 | type RWMutex struct { |
RWMutex.w
负责写锁的互相阻塞。
RWMutex.readerCount
标识goroutine的读锁数量。每次goroutine获得读锁,readerCount+1
。如果写锁被获取,那么readerCount在-rwmutexMaxReaders
与0之间。如果写锁未被获取,那么readerCount>=0
,获取读锁,不阻塞。
在RWMutex.readerCount
为负,也就是存在写锁的情况下,写锁Unlock时会释放读锁的信号量RWMutex.readerSem
,读锁RUnlock的时候会释放写锁的信号量RWMutex.writerSem
。(读锁不互相阻塞, 因此不需要信号量。写锁互相阻塞会通过RWMutex.w
实现)
WaitGroup
WaitGroup
除noCopy
字段,有意义的属性有三个:waiter
、counter
和sema
。(本来是[3]unit32
来存储,1.18版本进行拆分为uint64
和uint32
)
1 | type WaitGroup struct { |
和Java的CountDownLatch
功能类似,区别在于CountDownLatch
初始化之后不能+1
。常见使用模式:
1 | requests := []*Request{...} |
Once
Once
确保golang程序运行期间的某段代码只会执行一次。
数据结构如下,通过Mutex
和完成标识来实现。
type Once struct {
done uint32
m Mutex
}
Cond
go提供的条件变量,用途和Java中的synchronized
类似。Wait
用于等待,Broadcast
和Signal
分别解锁全部和单个。
不推荐使用,建议是用channel替换。
Pool
golang提供的对象池,可以通过服用对象,减轻gc的压力。
每个P
(GMP模型中的P)都绑定一个本地对象池poolLocal
。除本地对象池外,每个P
还有一个本地对象private
用于快速读写。
本地对象为空的时候,从P
对应本地对象池中获取,失败后则从其他P
的对象池偷。返还时若本地对象存在,则将返还对象放到对象池中。
1 | type Pool struct { |
Map
Map是线程安全版本的map[interface{}]interface{}
,通过读写分离实现,适用于读多写少(或者goroutine间读写不冲突)的情况。
1 | type Map struct { |
互斥量mu
保护read
和dirty
。
dirty
是一个非线程安全的原始map
,包含新写入的 key,和read
中的所有未被删除的key
。通过CAS,可以快速地将dirty
提升为read
对外提供服务。如果dirty
为nil
,那么下一次写入时,会新建一个新的dirty
,遍历read
中未删除(非nil
)的entry,迁移至dirty
,同时将read
的entry指针设置为expunged
。
每当从read
中读取失败,都会将misses
的计数值加 1。当加到一定阈值以后,将dirty
提升为read
,目前阈值为len(dirty)
。
entry
对read
和dirty
都是可见的。指针p
的状态有三种:
- 当
p == nil
时,说明这个键值对已被删除;此时dirty == nil
或者dirty[key]
指向当前entry。 - 当
p == expunged
时,说明这条键值对已被删除;此时dirty != nil
,且dirty[key] == nil
。 - 其他情况,
p
指向实际 interface{} 的地址。此时read.m[key]
和dirty[key]
实际上指向的是同一个值。