sync/mutex

实践总结

  1. 非可重入
  2. 对一个未加锁的mutex使用unlock会panic
  3. 注意,这种由 Go 语言运行时系统自行抛出的 panic 都属于致命错误,都是无法被恢复的,调用recover函数对它们起不到任何作用。也就是说,一旦产生死锁,程序必然崩溃
  4. 结构体类型,函数传递会产生副本。不过要注意,该类型是一个结构体类型,属于值类型中的一种。把它传给一个函数、将它从函数中返回、把它赋给其他变量、让它进入某个通道都会导致它的副本的产生。并且,原值和它的副本,以及多个副本之间都是完全独立的,它们都是不同的互斥锁。

源码解析

数据结构

type Mutex struct {
	state int32
	sema  uint32
}

锁状态state

低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放

  mutexLocked      = 1 << iota // mutex is locked  是否已经被锁
	mutexWoken                   // 是否有协程被唤醒
	mutexStarving                // 是否是饥饿模式
	mutexWaiterShift = iota      // 等待的协程数量

锁模式

为了保证锁的公平性,设计上互斥锁有两种状态:正常状态和饥饿状态

正常模式下,所有等待锁的 goroutine 按照 FIFO 顺序等待。唤醒的 goroutine 不会直接拥有锁,而是会和新请求锁的 goroutine 竞争锁。新请求锁的 goroutine 具有如下优势:它正在 CPU 上执行,而且可能有好几个,所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败。当新唤醒的goroutine竞争失败后,这个被唤醒的 goroutine 会加入到等待队列的前面。 如果队首的goroutine超过1ms没有获取锁,那么它将会把锁转变为饥饿模式。

饥饿模式下,锁的所有权将从 unlock 的 gorutine 直接交给交给等待队列中的第一个。新来的 goroutine 将不会尝试去获得锁,即使锁看起来是 unlock 状态, 也不会去尝试自旋操作,而是放在等待队列的尾部。

正常模式转饥饿模式条件

gorutine等待时间超过1ms(默认阈值)

饥饿模式转为正常模式的条件

如果一个等待的 goroutine 获取了锁,并满足一下条件之一,将锁的状态转换为正常状态。

(1) 它是队列中的最后一个;

(2) 它等待的时候小于 1ms。

饥饿模式的作用

使用饥饿模式有助于避免饥饿现象,从而降低系统任务调度的平均等待时间

Lock

总体流程

首先,通过一次cas操作,尝试获取锁,如果此时mutex没有被加锁也没有被唤醒的协程也没有处于饥饿模式,将加锁成功。如果加锁失败,则进入slowLock 加锁慢路径中。如果mutex未处于饥饿模式(饥饿模式锁将直接交给队首协程)且当前协程自旋次数没有超过阈值,当前协程将与唤醒的协程通过自旋竞争锁。竞争锁失败,将会直接挂起,陷入睡眠。

    func (m *Mutex) Lock() {
    	// 如果mutex的state没有被锁,也没有等待/唤醒的goroutine, 锁处于正常状态,那么获得锁,返回.
        // 比如锁第一次被goroutine请求时,就是这种状态。或者锁处于空闲的时候,也是这种状态。
    	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    		return
    	}
    	// Slow path (outlined so that the fast path can be inlined)
    	m.lockSlow()
    }
    
    func (m *Mutex) lockSlow() {
    	// 标记本goroutine的等待时间
    	var waitStartTime int64
    	// 本goroutine是否已经处于饥饿状态
    	starving := false
    	// 本goroutine是否已唤醒
    	awoke := false
    	// 自旋次数
    	iter := 0
    	old := m.state
    	for {
    		// 第一个条件:1.mutex已经被锁了;2.不处于饥饿模式(如果时饥饿状态,自旋时没有用的,锁的拥有权直接交给了等待队列的第一个。)
    		// 尝试自旋的条件:参考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 // go特有运算符,清除唤醒位
    		}
    
    		// 通过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
    			}
    			// If we were already waiting before, queue at the front of the queue.
    			// 设置并计算本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.
    				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
    			}
    			awoke = true
    			iter = 0
    		} else {
    			old = m.state
    		}
    	}
    }

UnLock

解锁总体流程

  1. 首先通过cas操作将mutex设为无锁状态,如果mutex原本是一个简单的锁(没有等待协程、非唤醒、非饥饿模式)则直接返回。否则进入释放锁慢路径
  2. 判断是否释放一个未加锁的mutex,是则panic
  3. 接着,再看是不是正常模式,是的话,尝试通过cas将mutex等待的协程梳理减一,同时将mutex状态设置为唤醒。状态设置成功,则唤醒队首协程。
  4. 如果mutex是饥饿模式,则直接将唤醒队首元素,将锁交给他

这里需要注意一点的是,释放锁过程,上来就会将mutex的锁状态设置为无锁状态,再执行后续释放锁其他操作。此时其他协程(刚进来的协程)将有可能修改锁的状态。

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// Fast path: drop lock bit.
	// 如何锁的state只是被锁,即没有唤醒、也不是饥饿模式也没有等待goroutine,直接抹除加锁状态返回
	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 {
		// 释放一个未加锁的mutex,panic
		fatal("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 {
				// 如果没有等待的goroutine或者已经唤醒了其他的协程就不需要再唤醒
				// 1. 对于刚释放锁的协程,第一次执行到这儿,由于之前已经将mutex的锁状态设为无锁了,所以这里不需要再去将锁状态设为无锁
				// 2. 再次执行到这里,由于释放锁的时候,上去就将锁状态位设为无锁,就有可能被新进入的goroutine抢占了,从而锁可能变为已上锁、唤醒和饥饿模式,此时该释放锁的协程也不需要做任何事情
				return
			}
			// Grab the right to wake someone.
			// 锁的等待goroutine数量减一,并设置为唤醒
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				// cas成功,唤醒队首goroutine争取锁
				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.
		// 饥饿模式,直接将锁给队首协程。
		runtime_Semrelease(&m.sema, true, 1)
	}
}

TryLock

func (m *Mutex) TryLock() bool {
	old := m.state
	// mutex已经上锁或者处于饥饿模式,失败
	if old&(mutexLocked|mutexStarving) != 0 {
		return false
	}

	// There may be a goroutine waiting for the mutex, but we are
	// running now and can try to grab the mutex before that
	// goroutine wakes up.
	// 此时mutex没有上锁也没有处于饥饿状态,通过cas尝试上锁,cas成功则trylock成功
	if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
		return false
	}

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

总结

更深入的探讨

到此,mutex核心代码讲述完毕,但是我们只是了解了其代码执行流程和核心逻辑。但是要想从全局去了解整个锁是如何设计和运行的则需要进行更加深入的分析。mutex state总体上说有十六种状态,是否加锁、是否有被唤醒的协程、是否是饥饿状态、等待协程数量(是否为0),总计共2^4=16种状态。要想了解mutex完整的逻辑,就需要了解这十六种状态的迁移方式,需要借助状态机等工具来阐述。

sync/mutex和Java AQS的区别

  1. AQS在创建AQS的时候需要指定获取锁是公平的和非公平的。相较之下mutex采用了自适应的算法,可以自动切换公平与非公平状态,有效避免饥饿现象。
  2. AQS实现基于CLH队列。同时在状态位的维护上AQS通过统计加锁的次数,实现了锁可重入
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值