互斥锁
简单版本:锁计数locks,信号量sem
1、加锁:
if atomicCompareAndSwap(locks,0,1) then
// 加锁成功
else then
插入sem等待队列尾部
end
2、解锁
if atomicCompareAndSwap(locks,1,0) > 0 then
尝试唤醒sem等待队列头部
end
该策略存在优化点:
1、活跃的协程获得锁时可尝试短暂自旋等待,不切换上下文
2、第1点带来的问题,活跃的协程频繁获得锁时会导致队列中任务等待时间过长,为此增加了饥饿模式
数据结构
// 源码路径:src/sync/mutex.go
type Mutex struct {
state int32 // 状态state。低3位分别:Lock加锁、Starve饥饿、Wake唤醒;高位:等待的任务数。如state.png
sema uint32 // 信号量
}
加锁
// 源码路径:src/sync/mutex.go
// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// 当前状态空闲,直接加锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false // 饥饿状态
awoke := false // 唤醒状态
iter := 0 // 自旋次数
old := m.state
for {
// Don't spin in starvation mode, ownership is handed off to waiters
// so we won't be able to acquire the mutex anyway.
// 饥饿模式下不自旋
// 普通模式下尝试自旋。多核可自旋不超过4次
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
// Try to set mutexWoken flag to inform Unlock
// to not wake other blocked goroutines.
// 标记唤醒状态
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
// Don't try to acquire starving mutex, new arriving goroutines must queue.
// 饥饿状态下仅唤醒等待队列中任务加锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 已加锁或饥饿状态,直接等待队列计数+1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
// 当前的任务被唤醒进入饥饿状态
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 已有任务唤醒,移除唤醒状态
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// 尝试更新状态
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 旧状态处于加锁或饥饿状态进入等待队列
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
queueLifo := waitStartTime != 0 // 先进后出
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 加入等信号量等待队列
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 被信号量唤醒后且处于饥饿状态
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<唤醒,进行调度优化,比较耗性能。
// 当前被唤醒的任务非饥饿状态或等待队列仅1个任务,可退出饥饿模式
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode.
// Critical to do it here and consider wait time.
// Starvation mode is so inefficient, that two goroutines
// can go lock-step infinitely once they switch mutex
// to starvation mode.
// 移除饥饿状态
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 unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 普通模式
if new&mutexStarving == 0 {
old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
// 无等待任务或状态被更新
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
// 尝试唤醒任务,等待任务计数-1
new = (old - 1<
读写锁
数据结构
读写锁是针对读多写少的场景。
读写锁需要记录多个数据:
1、正在读任务的数量 readCount。readCount<0:存在写任务
2、读任务信号量readSem。正在挂起的读队列
3、写任务信号量writeSem。正在挂起的写队列
4、读等待任务的数量readWait
5、互斥锁w
RLock 读加锁
// >0:加锁成功
// <0:存在写任务
if atomicAdd(readCount,1) < 0 then
readSem入队睡眠
end
RUnlock 读解锁
// >0:解锁成功
// <0:存在写任务
if atomicAdd(readCount,-1) < 0 then
// 0:当前最后一个读任务,可以唤醒写任务
if atomicAdd(readWait,-1) == 0 then
writeSem出队唤醒
end
end
Lock 写加锁
w.Lock()
r := atomicAdd(readCount,-writeShift)+writeShift
if r!=0 && atomicAdd(readWait,r) != 0 then
writeSem入队
end
Lock 写解锁
r := atomicAdd(readCount,writeShift)
for i=1,r {
readSem出队
}
w.Unlock()