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
*/

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) // 不加这行不会打印
}

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

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

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

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

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

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

将上面的代码执行这条命令 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等技术内容,立即学习

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

Go语言底层原理剖析》这本电子书主要深入探讨了Go语言底层实现原理和机制。在Go语言快速发展的背后,了解其底层原理对于深入理解和优化代码至关重要。 首先,书介绍了Go语言的内存管理。Go语言通过垃圾回收的方式自动管理内存,通过分代垃圾回收和并发标记等技术来提高垃圾回收的效率,并保证程序的性能。 其次,书Go语言的并发模型进行了详细解析。Go语言以轻量级的协程(goroutine)为基础,通过使用通道(channel)进行通信和同步,实现高效的并发编程。这本书从底层原理的角度深入剖析了协程的调度、通道的实现以及和同步原语等内容。 此外,书Go语言的编译器和运行时进行了解析。Go语言的编译器采用前端和后端分离的设计,通过词法分析、语法分析、类型检查和优化等步骤将Go源代码翻译成机器码。同时,Go语言的运行时系统提供了垃圾回收、调度器、内存管理等一系列功能,保证了程序的正确性和性能。 最后,本书还深入研究了Go语言的网络编程、文件IO、系统调用以及反射等底层机制。这些底层原理的理解,能够帮助开发者更好地理解和利用Go语言的特性,写出高效可靠的代码。 总之,《Go语言底层原理剖析》这本电子书通过深入剖析Go语言底层原理,为读者提供了深入理解Go语言的机会,有助于开发者更好地应用Go语言进行编程,写出高性能、可靠的代码。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值