【Golang】sync/mutex源码分析

先看数据结构:

type Mutex struct {
	state int32
	sema uint32
}

state代表Mutex的状态,sema是信号量。

以下常量表示了Mutex的状态:

const (
	mutexLocked = 1 << iota
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota
	starvationThresholdNs = 1e6
)

互斥锁的公平性:

互斥锁有两种状态:一种是normal,一种是starvation。在normal模式下,是一个FIFO的队列,但是注意在唤醒状态的没有所有权的会和新来的一起竞争。新的协程会有一定的优势,因为它们已经运行在CPU上。如果一个唤醒的超过1ms没有获取到锁,就会进入starvation模式。

在starvation模式下,mutex的所有权会直接从unlock的协程中交出给队列的第一个元素。新来的协程不会尝试去获取锁,也不会试图自旋。它们只会排在队尾。

如果获得锁的等待者是队尾的最后一个元素,并且等待的时间小于1ms,就会回到normal模式。

总的来说,Normal在一个锁可以连续的获取的情况下有更好的表现。starvation模式主要是为了避免病态。

Lock的实现:

func (m *Mutex) Lock() {
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	m.lockSlow()
}

如果mutex的状态为0,则置为1并返回,如果不是则进入lockSlow方法。

func (m *Mutex) lockSlow() {
	// 标记本goroutine的等待时间
	var waitStartTime int64
	// 标记本goroutine的等待时间
	starving := false
	// 本goroutine是否已唤醒
	awoke := false
	// 自旋次数
	iter := 0
	// 复制锁的当前状态
	old := m.state
	for {
		// 第一个条件是state已被锁,但是不是饥饿状态。如果时饥饿状态,自旋时没有用的,锁的拥有权直接交给了等待队列的第一个。
        // 第二个条件是还可以自旋,多核、压力不大并且在一定次数内可以自旋, 具体的条件可以参考`sync_runtime_canSpin`的实现。
        // 如果满足这两个条件,不断自旋来等待锁被释放、或者进入饥饿状态、或者不能再自旋。
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// 自旋的过程中如果发现state还没有设置woken标识,则设置它的woken标识, 并标记自己为被唤醒。
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		// 到了这一步, state的状态可能是:
        // 1. 锁还没有被释放,锁处于正常状态
        // 2. 锁还没有被释放, 锁处于饥饿状态
        // 3. 锁已经被释放, 锁处于正常状态
        // 4. 锁已经被释放, 锁处于饥饿状态
        //
        // 并且本gorutine的 awoke可能是true, 也可能是false (其它goutine已经设置了state的woken标识)
        // new 复制 state的当前状态, 用来设置新的状态
        // old 是锁当前的状态
		new := old
		// 如果old state状态不是饥饿状态, new state 设置锁, 尝试通过CAS获取锁,
        // 如果old state状态是饥饿状态, 则不设置new state的锁,因为饥饿状态下锁直接转给等待队列的第一个.
        if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// 将等待队列的等待者的数量加1
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// 如果当前goroutine已经处于饥饿状态, 并且old state的已被加锁,
        // 将new state的状态标记为饥饿状态, 将锁转变为饥饿状态.
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		// 如果本goroutine已经设置为唤醒状态, 需要清除new state的唤醒标记, 因为本goroutine要么获得了锁,要么进入休眠,
        // 总之state的新状态不再是woken状态.
		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
		}
		// 通过CAS设置new state值.
        // 注意new的锁标记不一定是true, 也可能只是标记一下锁的state是饥饿状态.
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// 如果old state的状态是未被锁状态,并且锁不处于饥饿状态,
            // 那么当前goroutine已经获取了锁的拥有权,返回
			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, 1)
			// sleep之后,此goroutine被唤醒
            // 计算当前goroutine是否已经处于饥饿状态.
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			// 得到当前的锁状态
			old = m.state
			// 如果当前的state已经是饥饿状态
            // 那么锁应该处于Unlock状态,那么应该是锁被直接交给了本goroutine
			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.
				// 如果当前的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 {
					// 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
				}
				// 设置新state, 因为已经获得了锁,退出、返回
				atomic.AddInt32(&m.state, delta)
				break
			}
			// 如果当前的锁是正常模式,本goroutine被唤醒,自旋次数清零,从for循环开始处重新开始
			awoke = true
			iter = 0
		} else {// 如果CAS不成功,重新获取锁的state, 从for循环开始处重新开始
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

unlock方法的实现:

扫描二维码关注公众号,回复: 10792259 查看本文章
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}
	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) {
	// 如果state不是处于锁的状态, 那么就是Unlock根本没有加锁的mutex, panic
	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, 1)
				return
			}
			old = m.state
		}
	} else {
		// 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.
		// 饥饿模式下, 直接将锁的拥有权传给等待队列中的第一个.
        // 注意此时state的mutexLocked还没有加锁,唤醒的goroutine会设置它。
        // 在此期间,如果有新的goroutine来请求锁, 因为mutex处于饥饿状态, mutex还是被认为处于锁状态,
        // 新来的goroutine不会把锁抢过去.
		runtime_Semrelease(&m.sema, true, 1)
	}
}
发布了433 篇原创文章 · 获赞 14 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105505357