一、核心结构
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
// 锁的状态 1表示上锁 32bit 1bit代表lock 1bit代表唤醒 1bit代表饥饿模式 剩下的代表waiter协程数量
state int32
sema uint32
}
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
const (
mutexLocked = 1 << iota // mutex is locked 1
mutexWoken // 2
mutexStarving // 4
mutexWaiterShift = iota // 3
// Mutex fairness.
//
// Mutes 可以处于两种操作模式:常规和饥饿
// 在常规模式下的waiter goroutine按照FIFO顺序排队
// 但是,当排队的goroutine被唤醒时,不会直接拥有mutex,而是会和刚到达争取mutex的goroutine竞争
// 新到达的goroutines具有优势,因为他们已经在cpu上运行,并且可能有很多,所以刚被唤醒的goroutine有很大的几率抢占cpu失败
// 在这种情况下,goroutine在等待队列的前面排队。如果goroutine获取互斥体的时间超过1毫秒,将从互斥的工作模式切换到饥饿状态
// Mutex can be in 2 modes of operations: normal and starvation.
// In normal mode waiters are queued in FIFO order, but a woken up waiter
// does not own the mutex and competes with new arriving goroutines over
// the ownership. New arriving goroutines have an advantage -- they are
// already running on CPU and there can be lots of them, so a woken up
// waiter has good chances of losing. In such case it is queued at front
// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
// it switches mutex to the starvation mode.
//
// 在饥饿模式下,互斥锁的所有权直接从解锁goroutine传递给队列最前面的goroutine。
// 新到达的goroutine不会尝试获取互斥锁,即使该互斥锁似乎已被解锁,也不会尝试自旋。而是,将自己排在等待队列的尾部。
// In starvation mode ownership of the mutex is directly handed off from
// the unlocking goroutine to the waiter at the front of the queue.
// New arriving goroutines don't try to acquire the mutex even if it appears
// to be unlocked, and don't try to spin. Instead they queue themselves at
// the tail of the wait queue.
// 如果waiter获得了互斥锁的所有权,并且它是队列中的最后一个waiter或者等待时间少于1 ms,互斥锁切换回正常操作模式
// If a waiter receives ownership of the mutex and sees that either
// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
// it switches mutex back to normal operation mode.
// 普通模式具有更好的性能,因为goroutine可以连续几次获取一个互斥锁,即使阻塞很多goroutine
// Normal mode has considerably better performance as a goroutine can acquire
// a mutex several times in a row even if there are blocked waiters.
//
// Starvation mode is important to prevent pathological cases of tail latency.
starvationThresholdNs = 1e6
)
Mutex主要通过维护state和sema来实现互斥锁
state是一个uint32,分成4部分
sema是信号量用来做pv操作
二、核心函数
1、Lock
// 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.
// 原子操作,抢占锁state
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.
// 被锁住且不是饥饿模式,并且可以自旋
// 判断是否需要自选,golang中自旋锁并不会一直自旋下去,在runtime包中runtime_canSpin方法做了一些限制, 传递过来的iter大等于4或者cpu核数小等于1,最大逻辑处理器大于1,至少有个本地的P队列,并且本地的P队列可运行G队列为空才会进行自旋。
// 利用位运算判断每一位上面的值
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.
// old>>mutexWaiterShift != 0 等待协程数不等于0
// !awoke协程还未被唤醒
// old&mutexWoken == 0 互斥锁还未被释放
// atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) CAS锁的唤醒状态设置成为被唤醒
// 尝试设置mutexWoken标志。
// 以防止Unlock时唤醒其他阻塞的goroutine。
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的锁标志位设置成1
new |= mutexLocked
}
// 饥饿模式或者被加上了锁
if old&(mutexLocked|mutexStarving) != 0 {
// new等待协程数加++
new += 1 << mutexWaiterShift
}
// 1011
// 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设置饥饿模式
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")
}
// a &^ b 的意思就是 清零a中,ab都为1的位
// 清除锁被唤醒标识
// 1011 0010 1001
new &^= mutexWoken
}
// 把state设置成new
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// old 全为0
// 没有锁,且不存在mutex不是饥饿状态
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)
//
// 已经是饥饿模式 或者 等待时间超过1ms 设置这个协程处于饥饿
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 拿一次新值
old = m.state
// mutex已经是饥饿状态
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.
// waiter数为0
// 锁住 或者 mutex处于woken
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 此协程不是饥饿 或者 waiter=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))
}
}
2、Unlock
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.
// old&(mutexLocked|mutexWoken|mutexStarving) != 0 --> old里面的mutexWoken标志位不等于0
// old&(mutexLocked|mutexWoken|mutexStarving) != 0 ==> old&mutexWoken != 0
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
// 设置标志位 woken
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 把mutex的所有权直接交个下一个waiter,并且产生一个cpu时间片以便下个waiter直接开始run
// Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
runtime_Semrelease(&m.sema, true, 1)
}
}
这个释放锁的函数,首先是做一个原子操作,释放lock的标志位,去除lock标志位之后,如果其他标志位还存在,进去下一步操作,判断是否是重复释放,重复释放直接panic。 通过mutexStarving标志位判断mutex是不是处于饥饿模式,如果处于饥饿态则直接把mutex的所有权直接交个下一个waiter,并且产生一个cpu时间片以便下个waiter直接开始run;如果处于正常模式,如果没有其他goroutine等待或者mutex处于唤醒状态,直接返回,否则把waiter num减一并且设置mutex成Woken,然后runtime_Semrelease(v)释放一个信号量去通知等待唤醒的goroutine
三、总结
1、位操作 &^
a &^ b 的意思就是 清零a中,ab都为1的位
a=1001 b=1000 ==> a &^ b = 0001
2、信号量
信号量(英语:semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态.
PV操作
计数信号量具备两种操作动作,称为V(signal())与P(wait())(即部分参考书常称的“PV操作”)。V操作会增加信号标S的数值,P操作会减少它。
运作方式:
初始化,给与它一个非负数的整数值。 运行P(wait()),信号标S的值将被减少。企图进入临界区段的进程,需要先运行P(wait())。当信号标S减为负值时,进程会被挡住,不能继续;当信号标S不为负值时,进程可以获准进入临界区段。 运行V(signal()),信号标S的值会被增加。结束离开临界区段的进程,将会运行V(signal())。当信号标S不为负值时,先前被挡住的其他进程,将可获准进入临界区段。