Golang的提供的同步机制有sync模块下的Mutex、WaitGroup以及语言自身提供的chan等。 这些同步的方法都是以runtime中实现的底层同步机制(cas、atomic、spinlock、sem)为基础的。
1 cas(Compare And Swap)和原子运算是其他同步机制的基础
2 自旋锁(spinlock)
自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,知直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态 Golang中的自旋锁用来实现其他类型的锁,与互斥锁类似,不同点在于,它不是通过休眠来使进程阻塞,而是在获得锁之前一直处于活跃状态(自旋)。
3 信号量
实现休眠和唤醒协程的一种方式。
信号量有两个操作P和V
P(S):分配一个资源
1. 资源数减1:S=S-1
2. 进行以下判断
如果S<0,进入阻塞队列等待被释放
如果S>=0,直接返回
V(S):释放一个资源
1. 资源数加1:S=S+1
2. 进行如下判断
如果S>0,直接返回
如果S<=0,表示还有进程在请求资源,释放阻塞队列中的第一个等待进程
golang中信号量操作:runtime/sema.go
P操作:runtime_Semacquire
V操作:runtime_Semrelease
type Mutex struct {
state int32 //0(可用) 1(被锁) 2~31等待队列计数
sema uint32 //信号量,向处于Gwaitting的G发送信号
}
const (
mutexLocked = 1 << iota // 1 互斥锁是锁定的
mutexWoken // 2 唤醒锁
mutexWaiterShift = iota // 2 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值
)
Lock方法申请对mutex加锁的时候分为两种情况:
//如果已经加锁,那么当前协程进入休眠阻塞,等待唤醒
func (m *Mutex) Lock() {
// 快速加锁:CAS更新state为locked
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
awoke := false //当前goroutine是否被唤醒
for {
old := m.state // 保存当前state的状态
new := old | mutexLocked // 新值locked位设置为1
// 如果当前处于加锁状态,新到来的goroutine进入等待队列
if old&mutexLocked != 0 {
new = old + 1<
UnLock 解锁分两步:
//锁没有和某个特定的协程关联,可以由一个协程lock,另一个协程unlock
func (m *Mutex) Unlock() {
if raceenabled {
_ = m.state
raceRelease(unsafe.Pointer(m))
}
// CAS更新state的状态为locked ======注意:解锁的瞬间可能会有新的协程到来并抢到锁
new := atomic.AddInt32(&m.state, -mutexLocked)
// 释放了一个没上锁的锁会panic:原先的lock位为0
if (new+mutexLocked)&mutexLocked == 0 {
panic("sync: unlock of unlocked mutex")
}
//判断是否需要释放资源
old := new
for {
/**
* 不需要唤醒的情况
* 1.等待队列为0
* 2.已经有协程抢到锁(上面的瞬间抢锁)
* 3.已经有协程被唤醒
*/
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
return
}
//将waiter计数位减一,并设置state为woken(唤醒)
//问:会同时有多个被唤醒的协程存在吗
new = (old - 1<
一代互斥锁的问题:
Lock 方法申请对 mutex 加锁的时候分三种情况:
func (m *Mutex) Lock() {
//快速加锁,逻辑不变
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
awoke := false
iter := 0
for {
old := m.state
new := old | mutexLocked
if old&mutexLocked != 0 { // 如果当前己经上锁,那么判断是否可以自旋
//短暂的自旋过后如果无果,就只能通过信号量让当前goroutine进入休眠等待了
if runtime_canSpin(iter) {
// Active spinning makes sense.
/**
* 自旋的操作:设置state为woken,这样在unlock的时候就不会唤醒其他协程.
* 自旋的条件:
* 1.当前协程未被唤醒 !awoke
* 2.其他协程未被唤醒 old&mutexWoken == 0
* 3.等待队列大于0
*/
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
//进行自旋操作
runtime_doSpin()
iter++
continue
}
new = old + 1<
二代自旋锁的问题:
基本逻辑:
LOCK流程:
type Mutex struct {
state int32
sema **uint32**
}
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
//为什么使用位掩码表达式
//第3位到第32位表示等待在mutex上协程数量
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving //新增饥饿状态
mutexWaiterShift = iota
starvationThresholdNs = 1e6 //饥饿状态的阈值:等待时间超过1ms就会进入饥饿状态
)
func (m *Mutex) Lock() {
//快速加锁:逻辑不变
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
var waitStartTime int64 //等待时间
starving := false //饥饿标记
awoke := false //唤醒标记
iter := 0 //循环计数器
old := m.state //保存当前锁状态
for {
// 自旋的时候增加了一个判断:如果处于饥饿状态就不进入自旋,因为饥饿模式下,释放的锁会直接给等待队列的第一个,当前协程直接进入等待队列
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// 当mutex不处于饥饿状态的时候,将new值设置为locked,也就是说如果是饥饿状态,新到来的goroutine直接排队
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 当mutex处于加锁锁或者饥饿状态时,新到来的goroutine进入等待队列
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 当等待时间超过阈值,当前goroutine切换mutex为饥饿模式,如果未加锁,就不需要切换
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// mutex 处于未加锁,正常模式下,当前 goroutine 获得锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// 如果已经在排队了,就排到队伍的最前面
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// queueLifo 为真的时候,当前goroutine会被放到队头,
// 也就是说被唤醒却没抢到锁的goroutine放到最前面
runtime_SemacquireMutex(&m.sema, queueLifo)
// 当前goroutine等待时间超过阈值,切换为饥饿模式,starving设置为true
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
//如果当前是饥饿模式
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 如果切换为饥饿模式,等待队列计数减1
delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
UnLock 解锁分两步:
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
// 正常模式
old := new
for {
/**
* 不需要唤醒的情况
* 1.等待队列为0
* 2.已经有协程抢到锁(上面的瞬间抢锁)
* 3.已经有协程被唤醒
* 4.处于饥饿模式 在饥饿模式获取到锁的协程仍然处于饥饿状态,新的goroutine无法获取到锁
*/
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<