文章计划分为以下几部分:
1.什么是锁,锁的粒度,一般锁的实现,常见锁的类型
并发下的同步和互斥
原子操作、信号量、互斥量、读写信号量、自旋锁
2.golang锁的设计思想与演进
3.golang锁设计的底层基础简介
4.golang锁的代码实现与一些流程图
5.吐槽~
参考资料:
设计演进http://www.cnblogs.com/niniwzw/archive/2013/06/24/3153955.html
一、什么是并发锁
高并发下,如何保证并行的多线程同时访问一片共享内存时不出现问题,加锁就是最简单的一种解决方案,通过保证上锁的原子操作来保证对资源访问权限的控制,其余的内存屏障等途径留待后续整理。
锁在不同场景下可以有很多分类,比如,分为乐观锁(读写锁)和悲观锁(互斥锁)
分为自旋锁、阻塞锁、重入锁、偏向锁、轻量锁和重量锁等。
锁的实现一般会依赖于信号量,信号量则是一个非负的整数计数器。
信号量:多线程同步使用的;一个线程完成某个动作后通过信号告诉别的线程,别的线程才可以执行某些动作;非负整数
互斥量:多线程互斥使用的;一个线程占用某个资源,那么别的线程就无法访问,直到该线程离开,其他线程才可以访问该资源;0或1
二、普通并发锁的设计思想
因为sync.mutex是悲观锁,也就是互斥锁,常规的互斥锁,可以通过信号量来进行控制。
具体互斥锁的实现原理可以参考这篇文章:https://www.cnblogs.com/sylz/p/6030201.html
简单来说,就是加锁时,就把信号量减一,如果是零说明加锁成功。释放锁时把信号量加一,如果是一说明释放成功。
按照这个场景,信号量和互斥量理论都可以完成,但是在实际应用中大家都使用信号量,因为信号量是多值得,可以通过信号量加等待队列,减少唤醒的次数。
三、golang的sync.mutex代码(1.10.3)
参考:https://studygolang.com/articles/1472
sema论文:https://swtch.com/semaphore.pdf
3.1mutex的代码
// 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
}
从代码可以看到Mutex的核心由两部分组成,state负责统计锁的抢占着,sema通过信号量来进行goroutine的调度等操作。
mutexLocked = 1(1):表示mutex处于锁状态。
mutexWoken = 2(10):表示mutex处于唤醒状态。
mutexStarving = 4 (100) :标识处于饥饿状态。(饥饿状态在此场景下应该怎么理解)
mutexWaiterShift = 3 :表示等待持有锁需要累计计数的左移位。
3.2Lock的代码
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
//1.最简单的情况,无人锁定时的加锁。
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
//可以忽略,这部分是特殊模式的代码
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
//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.
//2.1饥饿模式不允许自旋,控制权会被移交给等待方,非饥饿模式下允许自旋
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.
//2.1.1 在非awoke状态下,且没有被唤醒状态下,且没有排队的情况下,把状态记为awoke,防止其他自旋等待
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
//2.1.2 自旋等待,少量的同时自旋是可以容忍的。
runtime_doSpin()
iter++
old = m.state
continue
}
// 2.2 进入此处有两种情况,一种是未上锁状态,一种是自旋次数已经耗尽
// for循环是为了一次cas修改不成功时,自动进行下次cas修改
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
// 2.2.1如果没有优先状态的,才进行上锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 2.2.2如果现在在上锁状态,或者在优先调度状态,就增加一个等待标识
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.
// 2.2.3如果这是次优先调度,且还是上锁状态,就加抢占标签,把其他上锁操作
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 2.2.4 如果是唤醒状态的,且有唤醒标识,这次去掉
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
}
// 2.2.5 根据判断,设置新的状态,包括上锁成功和未上锁成功,加入排队序列等
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 2.2.5.1 上锁成功,就可以结束上锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
// 2.2.5.2 判断是否是被唤醒的未成功上锁的请求,如果是,加入队列头而不是队列尾部。
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 2.2.5.3 加入信号量队列,包括sleep和awoke等
runtime_SemacquireMutex(&m.sema, queueLifo)
// 2.2.5.4 如果等待超过1e6纳秒,就进入抢占状态,优先调度
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 2.2.5.5 如果已经在抢占状态下的处理,
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的代码,除去映射到runtime的自旋和信号量相关代码,我们已经可以完整的理解整个加锁的过程。对于自旋和信号量两部分的代码,估计需要之后结合golang的调度再做分析。
3.3Unlock代码
// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
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+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.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// Starving mode: handoff mutex ownership to the next waiter.
// 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)
}
}
解锁的代码相比加锁要简单的多,看懂了加锁代码,解锁代码的逻辑应该就很容易理解。
四、反思
sync.mutex的代码如果直接看1.10.3是非常难懂的,因为代码已经越来越复杂,加入了starve等概念,增加了代码的复杂度。如果可以,最好从中抽象出不带starve的代码,精简程度大大增加(参考文章中的1.3),通过精简版本,我们可以更快的理解其主流程。看懂主流程之后再回归到现有版本,会让我们的理解变得更加容易。
有疑问加站长微信联系(非本文作者)