互斥锁(百科)定义:“在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。”,顾名思义就是互相排斥的锁了。

当程序中就一个协程时,不需要加锁,但是实际工程中不会只有单协程,可能有很多协程同时访问公共资源,所以这个时候就需要用到锁,互斥锁的使用场景一般有:

  1. 多个协程同时读相同的数据时
  2. 多个协程同时写相同的数据时
  3. 同一个资源,同时有读和写操作时

读写锁之后,我们继续来说说互斥锁,互斥锁从原理上来说要比读写锁复杂一些,在Go语言中提供了sync.Mutex标准库,Mutex结构体来定义。Mutex同样继承于Locker接口。

互斥锁特点:一次只能一个协程拥有互斥锁,其他线程只有等待。

源码基于:go version go1.13.4 windows/amd64,sync包中Mutex、RWMutex的方法的inline化带来的性能提升,官方说法是10%。

两种操作模式:

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

两种工作模式:

  1. 竞争模式:所有协程一起抢
  2. 队列模式:所有协程一起排队

这两种工作模式会通过一些情况进行切换的。

互斥锁的定义

type Mutex struct {state int32  // 互斥锁上锁状态sema  uint32 // 信号量
}

state=0时是未上锁,state=1时是锁定状态。

互斥锁常量的定义

const (mutexLocked           = 1 << iota // 十进制:1,二进制:0001mutexWoken                        // 十进制:2,二进制:0010mutexStarving                     // 十进制:4,二进制:0100mutexWaiterShift      = iota      // 十进制:3,二进制:0011starvationThresholdNs = 1e6       // 1e+06
)

看一下互斥锁的结构主要方法,主要有Lock()和Unlonk()方法组成,使用Lock()加锁后便不能再次对其加锁操作,直到Unlock()解锁后才能再次加锁,适用于读写不确定的场景,并且只允许只有一个读或者写的场景。

Lock()

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()

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.statecontinue}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、互斥状态不相同就panicif 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 != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 11、进入休眠状态,等待信号唤醒runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 确认当前的锁的状态starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif old&mutexStarving != 0 {// 12、饥饿模式不会出现mutex被锁住|唤醒,等待队列不能为0if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}// 13、拿到锁,等待数-1delta := int32(mutexLocked - 1<<mutexWaiterShift)// 非饥饿模式,等待者只有一个时,退出饥饿模式if !starving || old>>mutexWaiterShift == 1 {delta -= mutexStarving}// 14、更新状态,高位原子计数,直接添加atomic.AddInt32(&m.state, delta)break}// 15、awoke=true,不处于饥饿模式,新到达的协程先获得锁awoke = trueiter = 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()

func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// 直接更新第一位即锁位置为0,直接解锁new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {m.unlockSlow(new)}
}

unlock()方法进入后直接更新第一位即锁位置为0,直接解锁,new!=0解锁失败后进入unlockSlow()方法进行解锁操作。

unlockSlow()

func (m *Mutex) unlockSlow(new int32) {// ①状态不一致,直接抛异常if (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}// ②饥饿模式直接唤醒队列首部的协程if new&mutexStarving == 0 {old := newfor {// ③如果没有等待者或协程,不用唤醒就返回if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// 等待者数量-1,将唤醒位改成1new = (old - 1<<mutexWaiterShift) | mutexWokenif 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位。

总结

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

本文为原创文章,出自guichenglin,转载请粘贴源链接,如果未经允许转发后果自负。

Go语言 读写锁互斥锁原理剖析(2)相关推荐

  1. Go语言 读写锁互斥锁原理剖析(1)

    我们在多协程操作时,有种场景是读操作次数远远大于写操作,这个时候,我们就会考虑用到读写锁. 读写锁 读写锁(百科)定义:是一种特殊的的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进 ...

  2. 对变量移位顺序读写_Java多线程并发读写锁ReadWriteLock实现原理剖析

    关于读写锁 Java语法层面的synchronized锁和JDK内置可重入锁ReentrantLock我们都经常会使用,这两种锁都属于纯粹的独占锁,也就是说这些锁任意时刻只能由一个线程持有,其它线程都 ...

  3. 自旋锁/互斥锁/读写锁/递归锁的区别与联系

    自旋锁 互斥锁 读写锁 递归锁 互斥锁(mutexlock): 最常使用于线程同步的锁:标记用来保证在任一时刻,只能有一个线程访问该对象,同一线程多次加锁操作会造成死锁:临界区和互斥量都可用来实现此锁 ...

  4. Java并发原理抽丝剥茧,读写锁ReadWriteLock实现深入剖析

    跟着作者的65节课彻底搞懂Java并发原理专栏,一步步彻底搞懂Java并发原理. 作者简介:笔名seaboat,擅长工程算法.人工智能算法.自然语言处理.架构.分布式.高并发.大数据和搜索引擎等方面的 ...

  5. python互斥锁原理_Linux 互斥锁的实现原理(pthread_mutex_t)

    引言 互斥锁大都会使用,但是要了解其原理就要花费一番功夫了.尽管我们说互斥锁是用来保护一个临界区,实际上保护的是临界区中被操纵的数据. 互斥锁还是分为三类:快速互斥锁/递归互斥锁/检测互斥锁 fute ...

  6. C++ 互斥锁原理以及实际使用介绍

    兄弟姐妹们,我又回来了,今天带来实际开发中都需要使用的互斥锁的内容,主要聊一聊如何使用互斥锁以及都有哪几种方式实现互斥锁.实现互斥,可以有以下几种方式:互斥量(Mutex).递归互斥量(Recursi ...

  7. java读写锁降级_java的读写锁中锁降级的问题

    读写锁是什么我就不多说了,下面说什么是锁降级 锁降级: 锁降级指的是写锁降级成为读锁.如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级.锁降级是指把持住(当前拥有 ...

  8. python互斥锁原理_python并发编程之多进程1------互斥锁与进程间的通信

    一.互斥锁 进程之间数据隔离,但是共享一套文件系统,因而可以通过文件来实现进程直接的通信,但问题是必须自己加锁处理. 注意:加锁的目的是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行 ...

  9. C语言 读写锁pthread_rwlock_init

    友链 gcc 1.c -o 1 -lpthread 更高效 不会阻塞读操作 // ..使用内存映射可以拷贝文件 /* 对原始文件进行内存映射 创建一个新文件 把新文件的数据拷贝映射到内存中 通过内存拷 ...

最新文章

  1. 【Android 逆向】ART 脱壳 ( InMemoryDexClassLoader 脱壳 | 加固厂商在 ART 下使用的两种类加载器 | InMemoryDexClassLoader 源码 )
  2. 【UWB】Savitzky Golay filter SG滤波器快速入门并上手使用
  3. java 远程 shell脚本_Java 远程调用 shell脚本
  4. [python]使用virtualenv处理python版本问题
  5. 前端经常遇到的跨域问题几种解决方案
  6. 学计算机用苹果本,新手小白用苹果电脑搞科研,学会这些才不至于尴尬!
  7. 苹果手机不装卡显示无服务器,现在的iPhone是不是没插卡都是显示无服务了 而不是无电话卡...
  8. MySQL的两阶段提交(数据一致性)
  9. AutoIt 快速入门指南
  10. Calc3: Partial Derivative
  11. 典型的人工神经网络由很多层构成,但不包括
  12. 大学本科计算机专业那些课 左飞
  13. 网站实用性是这样建出来的
  14. 人脸对齐之SDM论文解析
  15. Nokia计划在2009年推出多款OLED屏幕手机
  16. 【第三方API】顺丰API调用总结-java
  17. matlab误码率计算函数,matlab通信系统性能估计(误码率、误比特率、眼图、星座图….) | 学步园...
  18. vivado batch mode
  19. IPD(集成产品开发)
  20. MySQL按天查询语句

热门文章

  1. app应用内嵌h5页面怎么直接打开safari_localstroage过多存储满的情况下应该怎么办?...
  2. php7.0 java 性能,php7代码性能常见优化技巧
  3. mybatis in集合查询
  4. 一段树状无限制级代码
  5. JAVAOOP期末试题
  6. python filter map区别_python中filter、map、reduce的区别
  7. 对某自习室系统的一次渗透测试(从iot到getshell再到控制全国自习室)
  8. (C++)函数参数传递中的一级指针和二级指针
  9. 2020.11.me
  10. 数据分析项目某电商app行为数据分析(1)