什么是锁,为什么使用锁
用俗语来说,锁意味着一种保护,对资源的一种保护,在程序员眼中,这个资源可以是一个变量,一个代码片段,一条记录,一张数据库表等等。
就跟小孩需要保护一样,不保护的话小孩会收到伤害,同样的使用锁的原因是资源不保护的话,可能会受到污染,在并发情况下,多个人对同一资源进行操作,有可能导致资源不符合预期的修改。
常见的锁的种类
锁的种类细分的话,非常多,主要原因是从不同角度看,对锁的定义不一样,我这里总结了一下,画一个思维脑图,大家了解一下。
我个人认为锁都可以归为一下四大类,其它的叫法不同只是因为其实现方式或者应用场景而得名,但本质上上还是下面的这四大类中一种。
其它各种类的锁总结如下,这些锁只是为了高性能,为了各种应用场景在代码实现上做了很多工作,因此而得名,关于他们的资料很多
更多锁的详细解释参考我 github 的名词描述,这里不在赘述,地址如下:
https://github.com/sunpengwei1992/java_common/tree/master/src/lock
Go 中的锁使用和实现分析
Go 的代码库中为开发人员提供了一下两种锁:
互斥锁 sync.Mutex
读写锁 sync.RWMutex
第一个互斥锁指的是在 Go 编程中,同一资源的锁定对各个协程是相互排斥的,当其中一个协程获取到该锁时,其它协程只能等待,直到这个获取锁的协程释放锁之后,其它的协程才能获取。
第二个读写锁依赖于互斥锁的实现,这个指的是当多个协程对某一个资源都是只读操作,那么多个协程可以获取该资源的读锁,并且互相不影响,但当有协程要修改该资源时就必须获取写锁,如果获取写锁时,已经有其它协程获取了读写或者写锁,那么此次获取失败,也就是说读写互斥,读读共享,写写互斥。
1.同一个协程不能连续多次调用 Lock, 否则发生死锁
2.锁资源时尽量缩小资源的范围,以免引起其它协程超长时间等待
3.mutex 传递给外部的时候需要传指针,不然就是实例的拷贝,会引起锁失败
4.善用 defer 确保在函数内释放了锁
5.使用 - race 在运行时检测数据竞争问题,go test -race ....,go build -race ....
6.善用静态工具检查锁的使用问题
7.使用 go-deadlock 检测死锁,和指定锁超时的等待问题 (自己百度工具用法)
8.能用 channel 的场景别使用成了 lock
Mutex 实现中有两种模式,1:正常模式,2:饥饿模式,前者指的是当一个协程获取到锁时,后面的协程会排队 (FIFO), 释放锁时会唤醒最早排队的协程,这个协程会和正在 CPU 上运行的协程竞争锁,但是大概率会失败,为什么呢?因为你是刚被唤醒的,还没有获得 CPU 的使用权,而 CPU 正在执行的协程肯定比你有优势,如果这个被唤醒的协程竞争失败,并且超过了 1ms,那么就会退回到后者 (饥饿模式),这种模式下,该协程在下次获取锁时直接得到,不存在竞争关系,本质是为了防止协程等待锁的时间太长。
两种模式都了解了,我们再来分析一下几个核心常量,代码如下:
直接获取锁,返回
自旋和唤醒
判断各种状态,特殊情况处理
第一部分代码如下,较为简单,获取锁成功之后直接返回
用俗语来说,锁意味着一种保护,对资源的一种保护,在程序员眼中,这个资源可以是一个变量,一个代码片段,一条记录,一张数据库表等等。
就跟小孩需要保护一样,不保护的话小孩会收到伤害,同样的使用锁的原因是资源不保护的话,可能会受到污染,在并发情况下,多个人对同一资源进行操作,有可能导致资源不符合预期的修改。
常见的锁的种类
锁的种类细分的话,非常多,主要原因是从不同角度看,对锁的定义不一样,我这里总结了一下,画一个思维脑图,大家了解一下。
我个人认为锁都可以归为一下四大类,其它的叫法不同只是因为其实现方式或者应用场景而得名,但本质上上还是下面的这四大类中一种。
其它各种类的锁总结如下,这些锁只是为了高性能,为了各种应用场景在代码实现上做了很多工作,因此而得名,关于他们的资料很多
更多锁的详细解释参考我 github 的名词描述,这里不在赘述,地址如下:
https://github.com/sunpengwei1992/java_common/tree/master/src/lock
Go 中的锁使用和实现分析
Go 的代码库中为开发人员提供了一下两种锁:
互斥锁 sync.Mutex
读写锁 sync.RWMutex
第一个互斥锁指的是在 Go 编程中,同一资源的锁定对各个协程是相互排斥的,当其中一个协程获取到该锁时,其它协程只能等待,直到这个获取锁的协程释放锁之后,其它的协程才能获取。
第二个读写锁依赖于互斥锁的实现,这个指的是当多个协程对某一个资源都是只读操作,那么多个协程可以获取该资源的读锁,并且互相不影响,但当有协程要修改该资源时就必须获取写锁,如果获取写锁时,已经有其它协程获取了读写或者写锁,那么此次获取失败,也就是说读写互斥,读读共享,写写互斥。
Go 中关于锁的接口定义如下:,该接口的实现就是上面的两个锁种类,篇幅有限,这篇文章主要是分析一下互斥锁的使用和实现,因为 RWMutex 也是基于 Mutex 的,大家可以参考文章自行学习一下。
type Locker interface { Lock() Unlock() } type Mutex struct { state int32 //初始值默认为0 sema uint32 //初始值默认为0 }Mutex 使用也非常的简单,,声明一个 Mutex 变量就可以直接调用 Lock 和 Unlock 方法了,如下代码实例,但使用的过程中有一些注意点,如下:
1.同一个协程不能连续多次调用 Lock, 否则发生死锁
2.锁资源时尽量缩小资源的范围,以免引起其它协程超长时间等待
3.mutex 传递给外部的时候需要传指针,不然就是实例的拷贝,会引起锁失败
4.善用 defer 确保在函数内释放了锁
5.使用 - race 在运行时检测数据竞争问题,go test -race ....,go build -race ....
6.善用静态工具检查锁的使用问题
7.使用 go-deadlock 检测死锁,和指定锁超时的等待问题 (自己百度工具用法)
8.能用 channel 的场景别使用成了 lock
var lock sync.Mutex func MutexStudy(){ //获取锁 lock.Lock() //业务逻辑操作 time.Sleep(1 * time.Second) //释放锁 defer lock.Unlock() }我们了解了 Mutext 的使用和注意事项,那么具体原理是怎么实现的呢?运用到了那些技术,下面一起分析一下 Mutex 的实现原理。
Mutex 实现中有两种模式,1:正常模式,2:饥饿模式,前者指的是当一个协程获取到锁时,后面的协程会排队 (FIFO), 释放锁时会唤醒最早排队的协程,这个协程会和正在 CPU 上运行的协程竞争锁,但是大概率会失败,为什么呢?因为你是刚被唤醒的,还没有获得 CPU 的使用权,而 CPU 正在执行的协程肯定比你有优势,如果这个被唤醒的协程竞争失败,并且超过了 1ms,那么就会退回到后者 (饥饿模式),这种模式下,该协程在下次获取锁时直接得到,不存在竞争关系,本质是为了防止协程等待锁的时间太长。
两种模式都了解了,我们再来分析一下几个核心常量,代码如下:
const ( mutexLocked = 1 << iota //1, 0001 最后一位表示当前锁的状态,0未锁,1已锁 mutexWoken //2, 0010,倒数第二位表示当前锁是否会被唤醒,0唤醒,1未唤醒 mutexStarving //4, 0100 倒数第三位表示当前对象是否为饥饿模式,0正常,1饥饿 mutexWaiterShift = iota //3 从倒数第四位往前的bit表示排队的gorouting数量 starvationThresholdNs = 1e6 // 饥饿的阈值:1ms ) //Mutex中的变量,这里主要是将常量映射到state上面 state //0代表未获取到锁,1代表得到锁,2-2^31表示gorouting排队的数量的 sema //非负数的信号量,阻塞协程的依据这几个变量你要是都弄白了,那么代码看起来就相对好理解一些了,整个 Lock 的源码较长,我将注释写入代码中,方便大家理解,整个锁的过程其实分为三部分,建议大家参考源码和我的注释一块学习。
直接获取锁,返回
自旋和唤醒
判断各种状态,特殊情况处理
第一部分代码如下,较为简单,获取锁成功之后直接返回
//对state进行cas修改操作,修改成功相当于获取锁,修改之后state=1 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { return }第二部分自旋的代码如下
//开始等待时间 var waitStartTime int64 //这几个变量含义依次是:是否饥饿,是否唤醒,自旋次数,锁的当前状态 starving := false;awoke := false;iter := 0;old := m.state //进入死循环,直到获得锁成功(获得锁成功就是有别的协程释放锁了) for { //这个if的核心逻辑是判断:已经获得锁了并且不是饥饿模式 && 可以自旋,与cpu核数有关 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { //这个是判断:没有被唤醒 && 有排队等待的协程 && 尝试设置通知被唤醒 if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { //说明上个协程此时已经unlock了,唤醒当前协程 awoke = true } //自旋一段时间 runtime_doSpin() //自选次数加1 iter++ old = m.state continue } }第三部分代码,判断各种状态,特殊情况处理
new := old //1:原协程已经unlock了,对new的修改为已锁 if old&mutexStarving == 0 { new |= mutexLocked } //2:这里是执行完自旋或者没执行自旋(原协程没有unlock) if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift //排队 } //3:如果是饥饿模式,并且已锁的状态 if starving && old&mutexLocked != 0 { new |= mutexStarving //设置new为饥饿状态 } //4:上面的awoke被设置为true if awoke { //当前协程被唤醒了,肯定不为0 if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } //既然当前协程被唤醒了,重置唤醒标志为0 new &^= mutexWoken } //修改state的值为new,但这里new的值会有四种情况, //就是上面4个if情况对new做的修改,这一步获取锁成功 if atomic.CompareAndSwapInt32(&m.state, old, new) { if old&(mutexLocked|mutexStarving) == 0 { //这里代表的是正常模式获取锁成功 break } //下面的代码是判断是否从饥饿模式恢复正常模式 queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } //进入阻塞状态 runtime_SemacquireMutex(&m.sema, queueLifo) //设置是否为饥饿模式,等待的时间大于1ms就是饥饿模式 starving=starving||runtime_nanotime()-waitStartTime> starvationThresholdNs old = m.state //如果当前锁是饥饿模式,但这个gorouting被唤醒 if old&mutexStarving != 0 { if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } //减去当前锁的排队 delta := int32(mutexLocked - 1<<mutexWaiterShift) if !starving || old>>mutexWaiterShift == 1 { //退出饥饿模式 delta -= mutexStarving } //修改状态,终止 atomic.AddInt32(&m.state, delta) break } } //设置被唤醒 awoke = true iter = 0 } else { old = m.state }Lock 的源码我们弄明白了,那么 Unlock 呢,大家看代码的时候最好 Lock 和 Unlock 结合一起来看,因为他们是对同一变量 state 在操作
func (m *Mutex) Unlock() { //释放锁 new := atomic.AddInt32(&m.state, -mutexLocked) if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } //判断当前锁是否饥饿模式,==0代表不是 if new&mutexStarving == 0 { old := new for { //如果没有未排队的协程 或者 有已经被唤醒,得到锁或饥饿的协程,则直接返回 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } //唤醒其它协程 new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { runtime_Semrelease(&m.sema, false) return } old = m.state } } else { //释放信号量 runtime_Semrelease(&m.sema, true) } }到这里整个 Mutex 的源码分析完成,可以看到 Metux 的源码并不是很复杂,只是各种位运算让开发人员难以直接观察到结果值,另外阅读源码前一定要先明白各个变量和常量的含义,不然读起来非常费劲。
扫码二维码 获取免费视频学习资料
- 本文固定链接: http://phpxs.com/post/6949/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料
查 看2022高级编程视频教程免费获取