golang源码学习之sync.Mutex

479 阅读3分钟

一、核心结构

// 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部分 image.png
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不为负值时,先前被挡住的其他进程,将可获准进入临界区段。