kali锁屏后无法唤醒_Go语言 读写锁&互斥锁原理剖析(2)

前言

99567938a7c662cc62a57b1050ffc9df.png 互斥锁( 百科)定义:“在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。”,顾名思义就是 互相排斥的锁了。当程序中就一个协程时,不需要加锁,但是实际工程中不会只有单协程,可能有很多协程同时访问公共资源,所以这个时候就需要用到锁,互斥锁的使用场景一般有:
  1. 多个协程同时读相同的数据时
  2. 多个协程同时写相同的数据时
  3. 同一个资源,同时有读和写操作时
读写锁之后,我们继续来说说互斥锁,互斥锁从原理上来说要比读写锁复杂一些,在Go语言中提供了 sync.Mutex标准库, sync包中Mutex、RWMutex的方法的 inline 化带来的性能提升,官方说法是10%,Mutex结构体来定义。Mutex同样继承于Locker接口。 源码基于: go version go1.13.4 windows/amd64

互斥锁特点

99567938a7c662cc62a57b1050ffc9df.png一次只能一个协程拥有互斥锁,其他线程只有等待。

两种操作模式

99567938a7c662cc62a57b1050ffc9df.png
  1. 正常模式:所有协程以先进先出(FIFO)方式进行排队,被唤醒的协程同样需要竞争方式争夺锁,新协程争抢会有优势,因为他们已经运行在CPU上,更容易抢到锁,如果一个协程在等待超过1毫秒会自动切换到饥饿模式下。
  2. 饥饿模式:互斥锁会直接由解锁的协程交给队列头部的等待者,新争抢者不能直接获得锁,不尝试自旋,会老老实实的等。

两种工作模式

99567938a7c662cc62a57b1050ffc9df.png
  1. 竞争模式:所有协程一起抢
  2. 队列模式:所有协程一起排队,这两种工作模式会通过一些情况进行切换的。

互斥锁的定义

99567938a7c662cc62a57b1050ffc9df.png
type Mutex struct {  state int32  // 互斥锁上锁状态  sema  uint32 // 信号量}
state=0时是未上锁,state=1时是锁定状态。

互斥锁常量的定义

99567938a7c662cc62a57b1050ffc9df.png
> iota // 十进制:1,二进制:0001\n\tmutexWoken                        // 十进制:2,二进制:0010\n\tmutexStarving                     // 十进制:4,二进制:0100\n\tmutexWaiterShift      = iota      // 十进制:3,二进制:0011\n\tstarvationThresholdNs = 1e6       // 1e+06\n)","classes":[]}"   >
const (  mutexLocked           = 1 << iota // 十进制:1,二进制:0001  mutexWoken                        // 十进制:2,二进制:0010  mutexStarving                     // 十进制:4,二进制:0100  mutexWaiterShift      = iota      // 十进制:3,二进制:0011  starvationThresholdNs = 1e6       // 1e+06)
看一下互斥锁的结构主要方法,主要有Lock()和Unlonk()方法组成,使用Lock()加锁后便不能再次对其加锁操作,直到Unlock()解锁后才能再次加锁,适用于读写不确定的场景,并且只允许只有一个读或者写的场景。 69ed775bc106129994ddaf3ab7028abe.png

Lock()

99567938a7c662cc62a57b1050ffc9df.png
func (m *Mutex) Lock() {  // ①CAS尝试获取锁,state为0表示没有协程持有锁,直接获得锁,将mutexLocked置为1。  // 如果设置成功,直接返回。如果获取锁失败会进入lockSlow方法进行自旋抢锁,直到抢到锁后返回。  if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {    if race.Enabled {      race.Acquire(unsafe.Pointer(m))    }    return  }  m.lockSlow()}
首先、尝试CAS获取锁,这里直接调用CompareAndSwapInt32方法来原子操作检测锁的状态,可以加锁会将状态转为1,不可以加锁则状态为0。state为0表示没有协程持有锁,这个时候直接获得锁并将mutexLocked设置成1。如果设置成功了直接返回。如果获取锁失败了会进入lockSlow方法进行自旋抢锁,直到抢到锁为止。

lockSlow()

99567938a7c662cc62a57b1050ffc9df.png
> mutexWaiterShift\n\t\t}\n\t\t// 6、切换到饥饿模式,解锁时不切换\n\t\tif starving && old&mutexLocked != 0 {\n\t\t\tnew |= mutexStarving\n\t\t}\n\t\t// 7、唤醒\n\t\tif awoke {\n\t\t\t// 8、互斥状态不相同就panic\n\t\t\tif new&mutexWoken == 0 {\n\t\t\t\tthrow(\"sync: inconsistent mutex state\")\n\t\t\t}\n\t\t\t// 同时把awoke位清掉\n\t\t\tnew &^= mutexWoken\n\t\t}\n\n\t\tif atomic.CompareAndSwapInt32(&m.state, old, new) {\n\t\t\t// 9、old的第1位和第3位一定不是1,未锁定而且处于饥饿模式。获取锁成功\n\t\t\tif old&(mutexLocked|mutexStarving) == 0 {\n\t\t\t\tbreak // locked the mutex with CAS\n\t\t\t}\n\t\t\t// 10、被唤醒的协程抢锁失败,重新放到队列首部\n\t\t\tqueueLifo := waitStartTime != 0\n\t\t\tif waitStartTime == 0 {\n\t\t\t\twaitStartTime = runtime_nanotime()\n\t\t\t}\n\t\t\t// 11、进入休眠状态,等待信号唤醒\n\t\t\truntime_SemacquireMutex(&m.sema, queueLifo, 1)\n\t\t\t// 确认当前的锁的状态\n\t\t\tstarving = starving || runtime_nanotime()-waitStartTime < starvationThresholdNs\n\t\t\told = m.state\n\t\t\tif old&mutexStarving != 0 {\n\t\t\t\t// 12、饥饿模式不会出现mutex被锁住|唤醒,等待队列不能为0\n\t\t\t\tif old&(mutexLocked|mutexWoken) != 0 || old<>mutexWaiterShift)\n\t\t\t\t// 非饥饿模式,等待者只有一个时,退出饥饿模式\n\t\t\t\tif !starving || old<
func (m *Mutex) lockSlow() {  var waitStartTime int64 // 协程等待时间  starving := false       // 锁的模式  awoke := false          // 循环标记  iter := 0               // 计数器  old := m.state          // 当前的锁状态  for {    // 1、old&0101==0001等于1说明已经加过锁,(old第1位一定是1,第3位一定是0)这时候未处于饥饿模式,开始自旋。    if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {      // 2、当前协程未能成功更新mutexWoken位,mutexWoken位仍然为0,等待队列为空,更新mutexWoken成功开始自旋。      if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&        atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {        awoke = true      }      // 3、将当前的协程标识为唤醒状态后,执行自旋操作,计数器+1,当前状态更新到old。      runtime_doSpin()      iter++      old = m.state      continue    }    new := old    // 4、新到的协程第三位等于0为正常模式需要排除    if old&mutexStarving == 0 {      new |= mutexLocked    }    // 5、当old的1和3位为1时,为饥饿模式,需要去排队    if old&(mutexLocked|mutexStarving) != 0 {      new += 1 << mutexWaiterShift    }    // 6、切换到饥饿模式,解锁时不切换    if starving && old&mutexLocked != 0 {      new |= mutexStarving    }    // 7、唤醒    if awoke {      // 8、互斥状态不相同就panic      if new&mutexWoken == 0 {        throw("sync: inconsistent mutex state")      }      // 同时把awoke位清掉      new &^= mutexWoken    }    if atomic.CompareAndSwapInt32(&m.state, old, new) {      // 9、old的第1位和第3位一定不是1,未锁定而且处于饥饿模式。获取锁成功      if old&(mutexLocked|mutexStarving) == 0 {        break // locked the mutex with CAS      }      // 10、被唤醒的协程抢锁失败,重新放到队列首部      queueLifo := waitStartTime != 0      if waitStartTime == 0 {        waitStartTime = runtime_nanotime()      }      // 11、进入休眠状态,等待信号唤醒      runtime_SemacquireMutex(&m.sema, queueLifo, 1)      // 确认当前的锁的状态      starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs      old = m.state      if old&mutexStarving != 0 {        // 12、饥饿模式不会出现mutex被锁住|唤醒,等待队列不能为0        if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {          throw("sync: inconsistent mutex state")        }        // 13、拿到锁,等待数-1        delta := int32(mutexLocked - 1<        // 非饥饿模式,等待者只有一个时,退出饥饿模式        if !starving || old>>mutexWaiterShift == 1 {          delta -= mutexStarving        }        // 14、更新状态,高位原子计数,直接添加        atomic.AddInt32(&m.state, delta)        break      }      // 15、awoke=true,不处于饥饿模式,新到达的协程先获得锁      awoke = true      iter = 0    } else {      // 16、old = m.state,自旋没成功,更新new,记录当前的状态      old = m.state    }  }  if race.Enabled {    race.Acquire(unsafe.Pointer(m))  }}
lockSlow方法中首先设置循环标记awoke,初始化计数器,将当前锁的状态赋值给old等参数后进入循环,
1、old&0101==0001等于1说明已经加过锁,(old第1位一定是1,第3位一定是0)这时候未处于饥饿模式,开始自旋。
2、当前协程未能成功更新mutexWoken位,mutexWoken位仍然为0,等待队列为空,更新mutexWoken成功开始自旋。
3、将当前的协程标识为唤醒状态后,执行自旋操作,计数器+1,当前状态更新到old。
4、新到的协程第三位等于0为正常模式需要排除
5、当old的1和3位为1时,为饥饿模式,需要去排队
6、切换到饥饿模式,解锁时不切换
7、唤醒
8、互斥状态不相同就panic,同时把awoke位清掉
9、old的第1位和第3位一定不是1,未锁定而且处于饥饿模式。获取锁成功
10、被唤醒的协程抢锁失败,重新放到队列首部
11、进入休眠状态,等待信号唤醒,确认当前的锁的状态
12、饥饿模式不会出现mutex被锁住|唤醒,等待队列不能为0
13、拿到锁,等待数-1。非饥饿模式,等待者只有一个时,退出饥饿模式
14、更新状态,高位原子计数,直接添加
15、awoke=true,不处于饥饿模式,新到达的协程先获得锁
16、old = m.state,自旋没成功,更新new,记录当前的状态

Unlock()

99567938a7c662cc62a57b1050ffc9df.png
func (m *Mutex) Unlock() {  if race.Enabled {    _ = m.state    race.Release(unsafe.Pointer(m))  }  // 直接更新第一位即锁位置为0,直接解锁  new := atomic.AddInt32(&m.state, -mutexLocked)  if new != 0 {    m.unlockSlow(new)  }}
unlock()方法进入后直接更新第一位即锁位置为0,直接解锁,new!=0解锁失败后进入unlockSlow()方法进行解锁操作。

unlockSlow()

99567938a7c662cc62a57b1050ffc9df.png
>mutexWaiterShift) | mutexWoken\n\t\t\tif atomic.CompareAndSwapInt32(&m.state, old, new) {\n\t\t\t\t// 唤醒一个阻塞的协程,唤醒的不是第一个等待协程\n\t\t\t\truntime_Semrelease(&m.sema, false, 1)\n\t\t\t\treturn\n\t\t\t}\n\t\t\told = m.state\n\t\t}\n\t} else {\n\t\t// ④饥饿模式下,将持有锁交给下一个等待者,此时mutexLocked还为0,但是在饥饿模式下,新协程不会更新mutexLocked位。\n\t\truntime_Semrelease(&m.sema, true, 1)\n\t}\n}\n","classes":[]}"   >
func (m *Mutex) unlockSlow(new int32) {  // ①状态不一致,直接抛异常  if (new+mutexLocked)&mutexLocked == 0 {    throw("sync: unlock of unlocked mutex")  }  // ②饥饿模式直接唤醒队列首部的协程  if new&mutexStarving == 0 {    old := new    for {      // ③如果没有等待者或协程,不用唤醒就返回      if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {        return      }      // 等待者数量-1,将唤醒位改成1      new = (old - 1<      if atomic.CompareAndSwapInt32(&m.state, old, new) {        // 唤醒一个阻塞的协程,唤醒的不是第一个等待协程        runtime_Semrelease(&m.sema, false, 1)        return      }      old = m.state    }  } else {    // ④饥饿模式下,将持有锁交给下一个等待者,此时mutexLocked还为0,但是在饥饿模式下,新协程不会更新mutexLocked位。    runtime_Semrelease(&m.sema, true, 1)  }}
unlockSlow()方法进入后首先检查状态,如果状态不一致,直接抛异常。然后饥饿模式直接唤醒队列首部的协程,如果没有等待协程,就不唤醒直接返回。等待协程数量-1,将唤醒位改成1,唤醒一个阻塞协程,唤醒的不一定是第一个等待协程。否则、饥饿模式下,将持有锁交给下一个等待协程,此时mutexLocked还为0,但是在饥饿模式下,新的协程不会更新mutexLocked位。

总结

99567938a7c662cc62a57b1050ffc9df.png
  • 原子性,把一个互斥量锁定为一个原子操作,保证如果一个协程锁定了一个互斥量,这时候其他协程同一时间不能成功锁定这个互斥量。
  • 唯一性:如果一个协程锁定了一个互斥量,在他解锁之前,其他协程无法锁定这个互斥量。
  • 互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会无法加锁。如果在加锁前解锁,便会报错"panic: sync: unlock of unlocked mutex"。 
  • 互斥锁无冲突,有冲突时,首先自旋,经过短暂自旋后可以获得锁,如果自旋无结果时通过信号通知协程继续等待。

推荐阅读

  • Go 语言读写锁源码剖析(1)


喜欢本文的朋友,欢迎关注“Go语言中文网”:

7e04ea89bcdb4f06f395ec5cdea0e672.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值