内容简介:借鉴于Go夜读,加了个人理解:结构体
借鉴于 Go 夜读,加了个人理解: https://reading.developerlearning.cn/articles/sync/sync_mutex_source_code_analysis/
go版本:go1.12 windows/amd64
结构体
// 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 // 指代mutex锁当前的状态 sema uint32 // 信号量,用于唤醒gotoutine }
image.png
这里图片颜色有误,末尾3个1依次代表:mutex是否被加锁,mutex是否被唤醒,mutex当前是否处于饥饿状态。
几个常量
const ( mutexLocked = 1 << iota mutexWoken // 相当于 mutexWoken == 1<< 1 mutexStarving // 相当于 mutexStarving == 1<< 2 mutexWaiterShift = iota starvationThresholdNs = 1e6 )
mutexLocked值为1, 根据 mutex.state & mutexLocked
得到 mutex的加锁状态,结果为1表示已加锁,0表示未加锁
mutexWoken值为2(二进制:10),根据 mutex.state & mutexWoken
得到mutex的唤醒状态,结果为1表示已唤醒,0表示未唤醒
mutexStarving值为4(二进制:100),根据 mutex.state & mutexStarving
得到mutex的饥饿状态,结果为1表示处于饥饿状态,0表示处于正常状态
mutexWaiterShift值为3 ( 注:iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引))
,根据 mutex.state >> mutexWaiterShift
得到当前等待的goroutine数目
starvationThresholdN值为1e6纳秒,也就是1毫秒,当等待队列中队首goroutine等待时间超过starvationThresholdN,mutex进入饥饿模式。
饥饿模式与正常模式
Mutex有两种工作模式:正常模式和饥饿模式
在正常模式中,等待着按照FIFO的顺序排队获取锁,但是一个被唤醒的等待者有时候并不能获取mutex,它还需要和新到来的goroutine们竞争mutex的使用权。新到来的goroutine有一个优势,它们已经在CPU上运行且它们数量很多,因此一个被唤醒的等待者有很大的概率获取不到锁,在这种情况下它处在等待队列的前面。如果一个goroutine等待mutex释放的时间超过1ms,它就会将mutex切换到饥饿模式;
在饥饿模式中,mutex的所有权直接从解锁的goroutine递交到等待队列中排在最前方的goroutine。新到达的goroutine们不要尝试去获取mutex,即便它看起来是解锁状态,也不要尝试自旋,而是排到等待队列的尾部
如果一个等待者获取mutex的所有权,并且看到以下两种情况中的任一种: 1)它是等待队列中的最后一个, 或者2)它等待的时间少于1ms,它便将mutex切换回正常操作模式
——
函数
runtime_canSpin
自旋锁(spinlock)
:
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成[busy-waiting]。
golang对于自旋锁的取舍做了一些限制:1.多核; 2.GOMAXPROCS>1; 3.至少有一个运行的P并且local的P队列为空。golang的自旋尝试只会做几次,并不会一直尝试下去,感兴趣的可以跟一下源码。
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 on 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 } func sync_runtime_doSpin() { procyield(active_spin_cnt) }
runtime_doSpin
会调用procyield函数,该函数也是汇编语言实现。函数内部[循环]调用PAUSE指令。PAUSE指令什么都不做,但是会消耗CPU时间,在执行PAUSE指令时,CPU不会对它做不必要的优化。
runtime_SemacquireMutex
// 一个gotoutine的等待队列,如果lifo为true,则插入队列头,否则插入队尾
func runtime_SemacquireMutex(s *uint32, lifo bool)
runtime_Semrelease
// 唤醒被runtime_SemacquireMutex函数挂起的等待goroutine
// If handoff is true, pass count directly to the first waiter.
// 如果handoff为true,唤醒队列头第一个等待者,否则的话可能是随机
func runtime_Semrelease(s *uint32, handoff bool)
Lock
Lock方法申请对mutex加锁,Lock执行的时候,分三种情况
1. 无冲突
通过CAS操作把当前状态设置为加锁状态
2. 有冲突 开始runtime_canSpin自旋
,并等待锁释放,如果其他goroutine在这段时间内释放了该锁,直接获得该锁;如果没有释放进入3
3. 有冲突,且已经过了自旋阶段
通过调用seamacquire函数来让当前goroutine进入等待状态
func (m *Mutex) Lock() { // 查看 state 是否为0(空闲状态), 如果是则表示可以加锁,将其状态转换为1,当前 // goroutine加锁成功, 函数返回,获得锁 if atomic.CompareAndSwapInt32(&m.state,0,mutexLocked) { return } var waitStartTime int64 // 当前goroutine开始等待时间 starving := false // mutex 当前所处的模式 awoke := false // 当前 goroutine 是否被唤醒 iter := 0 // 自旋迭代的次数 old := m.state // old 保存当前 mutex 的状态 for { // 当mutex 处于加锁非饥饿工作模式且支持自旋操作的时候 if old &(mutexLocked | mutexStarving) == mutexLocked && runtime_canSpin(iter) { // 将 mutex.state 的倒数第二位设置为1,用来告 Unlock 操作,存在 goroutine 即将得到锁,不需要唤醒其他 goroutine if !awoke && old&muteWoken == 0 && old >> mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } // 自旋循环 runtime_doSpin() iter++ old = m.state continue } // 1.能进到此处,则表明不为加锁模式 new := old // 当 mutex 不处于饥饿状态的时候,将 new的第一位设置为 1,即 加锁 if old&mutexStarving == 0 { new |= mutexLocked } // 当mutex 处于加锁或饥饿状态的时候,新到来的goroutine进入等待队列 // 2.此处需要判断是否为加锁状态,因为从1到2的时候可能mutex 重新被其他goroutine加锁了 if old&(mutexLocked|mutexStarving) != 0 { new += 1<< mutexWaiterShift // 等待队列进1位 } // 当前 goroutine 将 mutex 切换为饥饿状态,但如果当前 mutex 未加锁,则不需要切换 Unlock 操作希望饥饿模式存在等待者 // 3.starving条件 是为了防止 如果在2处判断mutex没有处于加锁,而在这里判断mutex却加锁了,这时候加入饥饿模式,可是goroutine没有入列 if starving && old&mutexLocked != 0 { new |= mutexStarving } // 当前goroutine已经被唤醒 if awoke { // 当前 goroutine 被唤醒,将 mutex.state 倒数第二位重置 if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } new &^= mutexWoken } // 调用 CAS 更新 state 状态 if atomic.CompareAndSwapInt32(&m.state, old, new) { // mutex既不加锁也不饥饿,正常模式下,当前gotoutine获得锁,直接跳出 if old& (mutexLocked | mutexStarving) == 0 { break } // queueLifo 为 true 代表当前 goroutine 是等待状态的 goroutine queueLifo := waitStartTime != 0 if waitStartTime == 0 { // 记录开始等待时间 waitStartTime = runtime_nanotime() } // 将被唤醒却没得到锁的 goroutine 插入当前等待队列的最前端 runtime_SemacquireMutex(&m.sema, queueLifo) // 如果当前 goroutine 等待时间超过starvationThresholdNs,mutex 进入饥饿模式 starving = starving || runtime_nanotimne()-waitStartTime > starvationThresholdNs old = m.state if old&mutexStarving != 0 { // 如果为饥饿模式,但是不为加锁或者等待队列为0,抛异常 if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } // 等待状态的 goroutine - 1 delta := int32(mutexLocked - 1<<mutexWaiterShift) // 如果不是饥饿模式了或者当前等待着只剩下一个,退出饥饿模式 if !starving || old>>mutexWaiterShift == 1 { delta -= mutexStarving } // 更新状态 atomic.AddInt32(&m.state, delta) break } }else { old = m.state } } }
Unlock
Unlock方法释放所申请的锁
一个Mutex的lock方法并不跟一个特定的goroutine绑定,一个Mutex对象允许被一个goroutine lock,并被另一个goroutine unlock。
func (m *Mutex) Unlock() { // mutex 的state减去1, 加锁状态 -> 未加锁 new := atomic.AddInt32(&m.state, -mutexLocked) // 未 Lock 直接 Unlock(),报 panic if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } // mutex 正常模式 if new&mutexStarving == 0 { // 如果没有等待者,或者已经存在一个 goroutine 被唤醒或得到锁,或处于饥饿模式 // 无需唤醒任何处于等待状态的 goroutine // 因为lock方法存在自旋一直在获取锁,所以可能解锁后就已经有goroutine获取到锁了 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // 等待者数量减1,并将唤醒位改成1 new := (old - 1<<mutexWaitShift) | mutexWoken if atomic.ComnpareAndSwapInt32(&m.state, old, new) { // 唤醒一个阻塞的 goroutine,但不是唤醒第一个等待者 runtime_Semrelease(&m.sema, false) return } }else { // mutex 饥饿模式,直接将 mutex 拥有权移交给等待队列最前端的 goroutine runtime_Semrelease(&m.sema, true) } }
以上所述就是小编给大家介绍的《sync包 mutex源码阅读》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【源码阅读】AndPermission源码阅读
- 【源码阅读】Gson源码阅读
- 如何阅读Java源码 ,阅读java的真实体会
- 我的源码阅读之路:redux源码剖析
- JDK源码阅读(六):HashMap源码分析
- 如何阅读源码?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pro CSS and HTML Design Patterns
Michael Bowers / Apress / April 23, 2007 / $44.99
Design patterns have been used with great success in software programming. They improve productivity, creativity, and efficiency in web design and development, and they reduce code bloat and complexit......一起来看看 《Pro CSS and HTML Design Patterns》 这本书的介绍吧!