golang-sync.Mutex(互斥锁)源码刨析

903 阅读4分钟

基础结构

// 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 {
	state int32
	sema  uint32
}

state(有符号的32位整数) :

 高29位:当前阻塞degroutetine数				

 低3位:饥饿状态位,0正常莫模式,1饥饿模式	

 低2位:唤醒状态,0未唤醒,1已唤醒

 低1位:锁状态,0可用,1占用
 

sema : 信号量

几个常量

后面会用到,这里我们暂时明白每个变量的值

  • mutexLocked = 1 << iota // =1 表示已经上锁
  • mutexWoken // =2
  • mutexStarving // =4
  • mutexWaiterShift = iota // =3
  • starvationThresholdNs = 1e6 // 1毫秒

runtime方法介绍

提前介绍,这里只需要知道每个runtime方法是什么

  • runtime_canSpin

    golang内部实现的保守自旋锁,在runtime包中runtime_canSpin做了一些限制,传递过来的i大于4或者cpu核数小于等于1,最大逻辑处理器大于1,至少有个本地P队列,并且本地的P队列可运行G队列为空(最大逻辑处理器,本地的P队列和G队列可以自行下去了解)。

    func sync_runtime_canSpin(i int) bool {
    	// sync.Mutex is cooperative, so we are conservative with spinning.
    	// Spin only few times and only if running on a multicore machine and
    	// GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
    	// As opposed to runtime mutex we don't do passive spinning here,
    	// because there can be work on global runq or on other Ps.
    	if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
    		return false
    	}
    	if p := getg().m.p.ptr(); !runqempty(p) {
    		return false
    	}
    	return true
    }
    
  • runtime_doSpin

    会调用procyield函数,该函数也是汇编语言实现。函数内部循环调用PAUSE指令。PAUSE指 令什么都不做,但是会消耗CPU时间,在执行PAUSE指令时,CPU不会对它做不必要的优化。

    
    //go:linkname sync_runtime_doSpin sync.runtime_doSpin
    func sync_runtime_doSpin() {
    	procyield(active_spin_cnt)
    }    
    

Lock加锁

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota

	// Mutex fairness.
	//
	// 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.
	//
	// 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.
	//
	// 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.
	//
	// 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
)

// 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
	}

	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.
		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
		}
		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)
			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<<mutexWaiterShift)
				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))
	}
}

加锁的源码太过于长,下面我们进行拆分(时刻记住Lock方法是用来竞争锁的):

  • 位置1

    // Fast path: grab unlocked mutex.
    	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    		if race.Enabled {
    			race.Acquire(unsafe.Pointer(m))
    		}
    		return
    	}
    

    首先判断state状态是否为0,如果为0,证明没有协程持有锁, 这里直接获取到锁,将mutexLocked(=1)赋给state,然后返回。比如第一次被goroutine请求时,就是这样的状态,或者锁处于空闲的时候,也是这样的状态。

  • 位置2

    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.
    	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
    	}
    
    • waitStartTime:标记本次goroutine的等待时间
    • starving:本goroutine是否已经处于饥饿状态
    • awoke:本goroutine是否已唤醒
    • iter:自旋次数

    第一个if,是锁已经被占用,但是正常模式,goroutine还可以在一定次数内自旋。如果同时满足这两个条件,当前goroutine可以不断自旋来等待锁的释放,否则就进入饥饿模式。

    第二个if,自旋的过程中如果发现state还没有被设置为woken标示,则设置他的woken标示,并标记自己已被唤醒

  • 位置3

    	new := old
    	// Don't try to acquire starving mutex, new arriving goroutines must queue.
    	if old&mutexStarving == 0 {
    		new |= mutexLocked
    	}
    	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
    	}	
    

    到了这个地方,state的状态可能是:

    1. 锁还没有释放,锁处于正常状态
    2. 锁还没被释放,锁处于饥饿状态
    3. 锁已经被释放,锁处于正常状态
    4. 锁已经被释放,锁处于饥饿状态

    并且本goroutine的awoke可能是true,也可能是false()

    第一个if,如果当前锁的state处于饥饿状态,则不设置new state的锁,因为饥饿状态下锁直接转给等待队列的第一个。如果当前锁的state处于正常模式,则 new state 设置锁。

    第二个if,将等待者队列的等待着数量加1

    第三个if,如果当前goroutine已经是饥饿状态,并且 old state是已经被加锁了,则将new state 的状态标记为饥饿状态,将锁转变为饥饿状态。

    第四个if,如果本goroutine已经被设置为唤醒状态,需要清除new state的唤醒标记,因为本goroutine要么获得了锁,要么休眠了。总值state的新状态不再是woken状态。

  • 位置4

    if atomic.CompareAndSwapInt32(&m.state, old, new) {
    		if old&(mutexLocked|mutexStarving) == 0 {
    			break // locked the mutex with CAS
    		}
    		// 设置/计算本goroutine的等待时间
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            
            // 既然未能获取到锁, 那么就使用sleep原语阻塞本goroutine
            // 如果是新来的goroutine,queueLifo=false, 加入到等待队列的尾部,耐心等待
            // 如果是唤醒的goroutine, queueLifo=true, 加入到等待队列的头部
            runtime_SemacquireMutex(&m.sema, queueLifo)
            
            // sleep之后,此goroutine被唤醒
            // 计算当前goroutine是否已经处于饥饿状态.
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            // 得到当前的锁状态
            old = m.state
            
            // 如果当前的state已经是饥饿状态
            // 那么锁应该处于Unlock状态,那么应该是锁被直接交给了本goroutine
    		if old&mutexStarving != 0 { 
                
                // 如果当前的state已被锁,或者已标记为唤醒, 或者等待的队列中不为空,
                // 那么state是一个非法状态
                if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
                    throw("sync: inconsistent mutex state")
                }
                
                // 当前goroutine用来设置锁,并将等待的goroutine数减1.
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                
                // 如果本goroutine是最后一个等待者,或者它并不处于饥饿状态,
                // 那么我们需要把锁的state状态设置为正常模式.
                if !starving || old>>mutexWaiterShift == 1 {
                    // 退出饥饿模式
                    delta -= mutexStarving
                }
                
                // 设置新state, 因为已经获得了锁,退出、返回
                atomic.AddInt32(&m.state, delta)
                break
            }
            
            // 如果当前的锁是正常模式,本goroutine被唤醒,自旋次数清零,从for循环开始处重新开始
            awoke = true
            iter = 0
        } else { // 如果CAS不成功,重新获取锁的state, 从for循环开始处重新开始
            old = m.state
        }
    }
    

    第一个if, 通过CAS设置new state值,注意new的锁标记不一定是true, 也可能只是标记一下锁的state是饥饿状态

    第二个if,如果old state的状态是未被锁状态,并且锁不处于饥饿状态,那么当前goroutine已经获取了锁的拥有权,返回

Ulock释放锁

func (m *Mutex) Unlock() {
    // 如果state不是处于锁的状态, 那么就是Unlock根本没有加锁的mutex, panic
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }
    
    // 释放了锁,还得需要通知其它等待者
    // 锁如果处于饥饿状态,直接交给等待队列的第一个, 唤醒它,让它去获取锁
    // 锁如果处于正常状态,
    // new state如果是正常状态
    if new&mutexStarving == 0 {
        old := new
        for {
            // 如果没有等待的goroutine, 或者锁不处于空闲的状态,直接返回.
            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // 将等待的goroutine数减一,并设置woken标识
            new = (old - 1<<mutexWaiterShift) | mutexWoken
            // 设置新的state, 这里通过信号量会唤醒一个阻塞的goroutine去获取锁.
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                runtime_Semrelease(&m.sema, false)
                return
            }
            old = m.state
        }
    } else {
        // 饥饿模式下, 直接将锁的拥有权传给等待队列中的第一个.
        // 注意此时state的mutexLocked还没有加锁,唤醒的goroutine会设置它。
        // 在此期间,如果有新的goroutine来请求锁, 因为mutex处于饥饿状态, mutex还是被认为处于锁状态,
        // 新来的goroutine不会把锁抢过去.
        runtime_Semrelease(&m.sema, true)
    }
}