• go语言中的锁底层分析(二)



    作者简介:C/C++ 、Golang 领域耕耘者,创作者
    个人主页:作者主页
    专栏地址: 从原理解析go语言
    刷题专栏:leetcode专栏
    如果感觉博主的文章还不错的话,还请关注➕ 、点赞👍 、收藏🧡三连支持一下博主哦~~~


    这一节紧跟这 上一节内容的继续写

    💜 4. 只让看不让改的锁背后的原理

    4.1 由来

    当我们在多个协程同时只读时,只用互斥锁会很费很多性能,这时就有了读写锁,主要在下面三个情况用到

    1. 只读时,让其他人不能修改即可
    2. 只读时,多协程可以共享读
    3. 只读时,不需要互斥锁

    4.2 读写锁步骤原理分析

    1. 一般来说,会有两把锁
      读锁: 共享锁(多个协程或者线程可以共享)
      写锁: 是互斥锁
      在这里插入图片描述
    2. 多个协程进行读的时候,可以进行共享,如果来了写的协程,就不能获取锁,只能在等待队列中
      在这里插入图片描述
    3. 当读的协程全部执行结束后,写的协程被唤醒,这时只能一个写协程能获取到锁,其他的只能在
      等待队列中
      在这里插入图片描述
    4. 只能等待
      在这里插入图片描述
    5. 如果这时来了一个读锁协程呢, 这是读的协程也是得不到锁的,因为读锁和写锁是不兼容的
      在这里插入图片描述
    6. 这时就会等待,所以读写锁一般有两个等待队列,一个是读等待队列,一个是写等待队列
      在这里插入图片描述

    4.3 读写锁需求

    1. 每个锁分为读锁和写锁,写锁互斥
    2. 没有加写锁时,多个协程都可以加读锁
    3. 加了写锁时,无法加读锁,读协程排队等待
    4. 加了读锁,写锁排队等待

    4.4 读写锁数据结构解释

    源码
    在这里插入图片描述
    w : 互斥锁作为写锁
    writerSem : 作为写协程队列
    readerSem : 作为读协程队列
    readerCount: 正值: 正在读的协程 负值: 加了写锁
    readerWait : 写锁应该等待读协程个数(还有等待几个读协程释放)

    数据结构示意图:
    在这里插入图片描述

    4.5 加写锁

    4.5.1 流程步骤

    1. 首先这个锁是初始状态时
      写锁: 首先是加上w 互斥锁
      在这里插入图片描述
    2. 这样才算加写锁成功了
      在这里插入图片描述
    3. 有读协程时,表示有3个协程加了读锁
      在这里插入图片描述
    4. 先加上互斥锁,这里需要减去 rwmutexMaxReaders , 变成负数,阻塞读锁的获取,然后下面有标记需要等待3个读锁协程释放,然后协程放入等待队列

    在这里插入图片描述

    1. 等待的协程在等待队列里,需要等待 readerWait 个协程释放锁资源,才能被唤醒readerWait 记录了下一个写协程什么时候唤醒
      在这里插入图片描述

    4.5.2 流程总结

    1. 先加mutex 写锁, 若已经被加写锁会阻塞等待
    2. 将readerCount 变为负值,阻塞读锁的获取
    3. 计算需要等待多少个读协程释放
    4. 如果需要等待读协程释放,陷入writerSem (写协程队列等待)

    在这里插入图片描述
    【注】:代码在RWMutex 中Lock函数

    4.6 解写锁

    4.6.1 流程步骤

    1. 初始状态
      在这里插入图片描述

    2. 会加上一个数, 然后把读协程放出来进去读取,之后锁释放

    在这里插入图片描述

    1. 释放锁

    在这里插入图片描述

    4.6.2 流程总结

    1. 将readerCount 变为正值, 允许读锁的获取
    2. 释放在readerSem 中等待的读协程
    3. 解锁mutex

    4.7 加读锁(readerCount > 0)步骤

    1. 已经有2的读协程,没有写协程干预, 这个情况简单,直接加上就行了
      在这里插入图片描述
    2. 把2 变为3 就行了
      在这里插入图片描述

    4.8 加读锁(readerCount < 0)步骤

    1. 有写锁在, 里面是负值
      在这里插入图片描述
    2. 这时候会进入到等待队列中
      在这里插入图片描述

    4.9 加读锁(总结)

    1. 将给readerCount 无脑加1
    2. 如果readerCount 是正数, 加锁成功
    3. 如果readerCount 是负数,说明被加了写锁,陷入readerSem

    4.10 解读锁

    4.10.1 readerCount > 0

    在这里插入图片描述
    很简单, 直接readerCount 减 1

    4.10.2 readerCount < 0

    1. 表示有写协程等待, 这里要等3个读协程的释放

    在这里插入图片描述

    1. 这里两边都减 1 ,给写协程维护值
      在这里插入图片描述

    2. 直到减为1, 就说明写协程可以释放了
      在这里插入图片描述

    4.10.3 小结

    1. 给readerCount 无脑减 1
    2. 如果readerCount 是正数,解锁成功
    3. 如果readerCount 是负数,有写锁在排队(如果自己是readerWait 的最后一个,唤醒写协程)

    4.11 使用经验

    1. RW 锁适合读多写少的场景,减少锁冲突

    4.12 小结

    1. Mutex 用来写协程之间互斥等到
    2. 读协程使用readerSem 等待写锁的释放
    3. 写协程使用writeSem 等待读锁的释放
    4. readerCount 记录读协程个数
    5. readerWait 记录写协程之前的读协程个数

    🤎 5. 如何通过WaitGroup 互相等待?

    5.1 WaitGroup例子

    还是上面场景

    package main
    import (
    	"fmt"
    	"sync"
    ) 
    type Person struct {
    	mtx sync.RWMutex
    	salary int
    	level int
    } 
    func (p *Person) promote() {
    	p.mtx.Lock()
    	p.salary += 1000
    	p.level += 1
    	fmt.Println(p.salary)
    	fmt.Println(p.level)
    	p.mtx.Unlock()
    } 
    func (p *Person) print(w *sync.WaitGroup) {
    	p.mtx.RLock()
    	defer p.mtx.RUnlock()
    	fmt.Println(p.salary)
    	fmt.Println(p.level)
    	w.Done() // 不止适用于主协程
    } 
    func main() {
    	p := Person{level: 1, salary: 10000}
    	wg := sync.WaitGroup{}
    	wg.Add(3) // 有多少任务
    	// 模拟三个人给小伙p 升职加薪
    	go p.print(&wg)
    	go p.print(&wg)
    	go p.print(&wg)
    	wg.Wait() // 等不到3个任务执行的Done 就会卡在这里
    	// 在主协程中用这个防止退出,如果嫌弃太low, 可换为waitgroup
    	//time.Sleep(time.Second)
    } 
    /**
    不加锁
    11000
    4
    13000
    12000
    4
    4 
    加锁
    11000
    2
    12000
    3
    13000
    4
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    5.2 一个协程需要等待另一个协程完成的场景

    现在有一个需求: 实际业务中,一个(组)协程需要等待另一组协程完成

    在这里插入图片描述
    如果想要实现等待组,分析得出,需要记录下面3个东西
    在这里插入图片描述

    5.3 sync.WaitGroup

    5.3.1 数据结构

    在这里插入图片描述

    5.3.2 Wait 函数

    1. 如果被等待的协程没了,直接返回
    2. 否则,waiter 加1, 陷入sema
      在这里插入图片描述

    5.3.3 Done 函数

    1. 被等待协程做完,给count 减1
    2. 通过Add(-1) 实现

    在这里插入图片描述

    5.3.4 Add 函数

    1. add counter
    2. 被等待协程没做完,或者没人在等待,返回
    3. 被等待协程都做完(减到0),且有人在等待,唤醒所有sema 中的协程
      在这里插入图片描述

    5.4 小结

    1. WaitGroup实现了一组协程等待另一组协程
    2. 等待的协程陷入sem 并记录个数
    3. 被等待的协程计数归零时,唤醒所有sema 中的协程

    🖤 6. 让一段代码只执行一次,如何去实现?

    6.1 需求

    1. 整个程序运行过程中,代码只执行一次
    2. 用来进行一些初始化操作

    6.2 代码

    package main
    import (
    "fmt"
    "sync"
    "time"
    ) 
    type Person struct {
    	mtx sync.RWMutex
    	salary int
    	level int
    }
    func (p *Person) promote() {
    	p.mtx.Lock()
    	p.salary += 1000
    	p.level += 1
    	fmt.Println(p.salary)
    	fmt.Println(p.level)
    	p.mtx.Unlock()
    } 
    func (p *Person) print(w *sync.WaitGroup) {
    	p.mtx.RLock()
    	defer p.mtx.RUnlock()
    	fmt.Println(p.salary)
    	fmt.Println(p.level)
    	w.Done() // 不止适用于主协程
    } 
    func main() {
    	p := Person{level: 1, salary: 10000}
    	once := sync.Once{}
    	go once.Do(p.promote)
    	go once.Do(p.promote)
    	go once.Do(p.promote)
    	time.Sleep(time.Second) // 不加这行不会打印
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    6.3 思路

    找一个变量记录一下,从0变成1 就不在做了

    6.4 思路实现1:atomic

    做法: 通过CAS 改值,成功就做
    优点: 算法非常简单
    问题: 多个协程竞争CAS 改值会造成性能问题

    func main() {
    	p := Person{level: 1, salary: 10000}
    	once := sync.Once{}
    	o := int32(0)
    	atomic.CompareAndSwapInt32(&o, 0, 1)
    	// 然后到实现的函数中判断这个值是否为1
    	go once.Do(p.promote)
    	go once.Do(p.promote)
    	go once.Do(p.promote)
    	time.Sleep(time.Second)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    6.5 思路实现2:mutex(参考源码)

    1. 争抢一个mutex, 抢不到的陷入sema 休眠
    2. 抢到的执行代码,改值,释放锁
    3. 其他协程唤醒后判断值已经修改,直接返回

    6.5.1 步骤

    1. 参考里面的两个变量
      在这里插入图片描述
    2. 就一个Do 函数, 先判断做没做
      在这里插入图片描述
    3. 获取到这把锁之后再次判断

    在这里插入图片描述

    6.6 sync.Once 使用

    1. 先判断是否已经改值
    2. 没改,尝试获取锁
    3. 获取到锁的协程执行业务,改值,解锁
    4. 冲突协程唤醒后直接返回

    6.7 小结

    1. sync.Once 实现了一段代码只执行一次
    2. 使用标志+mutex 实现了并发冲突的优化

    🤍 7. 实战如何排查锁异常问题

    7.1 锁拷贝问题

    7.1.1 什么是锁拷贝

    m := sync.Mutex{}
    m.Lock()
    /
    n := m
    
    m.Unlock()
    //在下面是锁不上的,n 是复制上面的那个状态
    n.Lock()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意事项:
    永远不要拷贝锁, 直接新建一个锁, n := sync.Mutex{}

    上面的例子看似简单,但在实际中,可能出现拷贝结构体, 结构体中有锁的情况,如果直接进行
    拷贝就会出现上面的情况;
    这种情况也不好发现, 例如下面的代码

    p := Person{level: 1, salary: 10000}
    p1 := p
    
    • 1
    • 2

    7.1.2 使用vet 工具可以检测锁拷贝问题

    在程序中可以执行这条命令go vet main.go,如果出现所拷贝情况, 则会报下面错误
    在这里插入图片描述
    【注】: vet 还能检测可能的bug 或者可疑的构造

    7.2 RACE 竞争检测

    7.2.1 什么是RACE 竞争

    不好用语言描述,用一个程序来进行表达

    代码如下:

    package main
    import (
    "fmt"
    "sync"
    ) 
    type Person struct {
    	mtx sync.RWMutex
    	salary int
    	level int
    } 
    func (p *Person) promote() {
    	p.salary += 1000
    	p.level += 1
    	fmt.Println(p.salary)
    	fmt.Println(p.level)
    } 
    func main() {
    	p := Person{level: 1, salary: 10000}
    	for i := 0; i < 200; i++ {
    		go p.promote()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    将上面的代码执行这条命令 go build -race main.go,然后执行生成的文件, 这就是race 竞争检测
    在这里插入图片描述

    7.2.2 意义

    1. 发现隐含的数据竞争问题
    2. 可能是加锁的建议
    3. 可能是bug 的提醒

    7.3 go-deadlock 检测

    这有一个开源的三方工具,地址如下https://github.com/sasha-s/go-deadlock

    7.3.1 使用方法

    直接用这个包, 这个包是继承sync.Lock 的,
    但是在代码中出现死锁可以提醒

    7.3.2 工具分析

    1. 检测可能的死锁
    2. 实际是检测获取锁的等待时间
    3. 用来排查bug 和性能问题

    7.4 小结

    1. go vet 检测bug 或者可疑的构造
    2. race 发现隐含的数据竞争问题
    3. go-deadlock 检测可能的死锁

    8. 总结

    8.1 atomic 机制

    1. 原子操作是一种硬件层面加锁的机制
    2. 将多个操作集中到一个原子指令中
    3. 数据类型和操作类型有限制

    8.2 sema

    sema 这个东西往往的被作为的等待队列,贯穿整章
    (极少自己使用,往往配合其他东西用)

    1. 一种锁机制,以一个uint32 值作为计数器
    2. uint32 值代表同时可并发的数量
    3. uint32 = 0时,sema 锁退化成一个专用休眠队列

    8.3 Mutex 互斥锁

    1. 协程获取不到互斥锁会自旋重试
    2. 重试多次会使用sema 机制休眠排队
    3. 长时间获取不到会饥饿
    4. 饥饿时不自旋,严格按照sema 队列排序

    8.4 RW 锁

    1. RW 锁适合读多写少的场景,减少锁冲突
    2. RW 底层使用了一个Mutex 和两个sema

    8.5 WaitGroup

    1. WaitGroup 实现了一组协程等待另一组协程
    2. 等待的协程陷入sema 并记录个数
    3. 被等待的协程计数归零时,唤醒所有sema 中的协程

    8.6 once

    1. sync.Once 实现了一段代码只执行一次
    2. 使用标志 + mutex 实现了并发冲突的优化

    8.7 如何排查锁异常问题

    1. go vet 检测bug 或者可疑的构造
    2. race 发现隐含的数据竞争问题
    3. go-deadlock 检测可能的死锁

    推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

    如果此篇博客对你有帮助的话,可以动一动你的小手~~~
    👍 点赞,你的认可是我创作的动力!
    🧡 收藏,你的青睐是我努力的方向!
    ✏️ 评论,你的意见是我进步的财富!

  • 相关阅读:
    Vue.js 框架源码与进阶 - Vue.js 3.0 响应式系统原理
    vue基础知识八:为什么data属性是一个函数而不是一个对象?
    Vue ref获取元素和组件实例
    入门力扣自学笔记123 C++ (题目编号768)
    敲了几万行源码后,我给Mybatis画了张“全地图”
    选择远程办公,选择放弃远程办公
    双非二本找实习前的准备day3
    达梦:dmfldr 数据装载
    数学建模--逻辑回归算法的Python实现
    22 年国内最牛的 Java 面试八股文合集(全彩版),不接受反驳
  • 原文地址:https://blog.csdn.net/qq_39486027/article/details/126287557