当前位置: 首页 > article >正文

Go锁 详解

- Go 函数并发编程中,锁是一种同步机制,用于协调对共享资源的访问,防止数据竞争
- Go 中提供了多种类型的锁,每种锁都有不同的特性和适用场景

类型

  • 互斥锁(mutex)

    • 基础锁,只能同时允许一个 goroutine 获取资源(悲观锁)
    • 保证了对共享资源的独占访问
    • 适用于对数据进行频繁写操作的场景
  • 读写锁(RWMutex)

    • 更高级的锁,它允许多个goroutine同时读取受保护的数据,但只允许一个goroutine同时写入(悲观锁)
    • 可以提高程序的性能,因为读取操作通常比写入操作要快
    • 适用于对数据进行频繁读操作的场景

互斥锁

  • 底层结构
// sync 包下的mutex就是互斥锁
type Mutex struct {
 state int32
 sema  uint32
}
- state:表示当前互斥锁的状态,复合型字段
- sema:信号量变量,用来控制等待goroutine的阻塞休眠和唤醒

state的不同位分别表示了不同的状态,使用最小的内存来表示更多的意义
在这里插入图片描述

// 其中低三位由低到高分别表示mutexLocked、mutexWoken 和 mutexStarving
// 剩下的位则用来表示当前共有多少个goroutine在等待锁:
const (
   mutexLocked = 1 << iota // 表示互斥锁的锁定状态
   mutexWoken // 表示从正常模式被从唤醒
   mutexStarving // 当前的互斥锁进入饥饿状态
   mutexWaiterShift = iota // 当前互斥锁上等待者的数量
)
提供了三个公开方法:
Lock():获得锁,Unlock():释放锁,在Go1.18新提供了TryLock()方法可以非阻塞式的取锁操作
  • 加锁
    加锁

  • 释放锁
    在这里插入图片描述

  • 正常模式(默认)

    • 采用公平的先进先出策略
    • 当一个goroutine尝试获取锁时,如果锁处于加锁状态,该goroutine会被放入等待队列中,等待锁的释放。当锁被解锁后,等待队列中的goroutine会按照先后顺序获取锁
    • 当一个协程被唤醒后并不是直接拥有锁,该协程需要和刚刚到达的协程一起竞争锁的所有权
    • 当等待的 goroutine 1ms内没有获取到锁,将会把锁置为饥饿模式
  • 饥饿模式

    • 非公平的模式
    • 互斥锁会直接交给等待队列最前面的goroutine,新的 goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待
    • 当等待队列中的协程获取到锁,它会查看以下俩个条件,有任意一个满足,将会将锁改为普通模式
      1. 自己是否是等待队列中最后一个协程
      2. 自己等待的时间是否小于1ms
  • 自旋

    • 定义:
      • 加锁时,如果发现该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测锁是否被释放
      • 自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁
    • 优势:
      • 为了更加高效,减少损耗,更充分的利用CPU,尽量避免协程切换
      • 当前申请加锁的协程拥有CPU,如果经过短时间的自旋可以获得锁,当前协程可以继续运行,不必进入阻塞状态
    • 条件:
      • 自旋次数要足够小,通常为4,即自旋最多4次
      • CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
      • 调度机制中的Process数量要大于1,否则自旋没有意义
      • 调度机制中的可运行队列必须为空,否则会延迟协程调度,需要把CPU让给更需要的进程
    • 问题:
      • 自旋有个特性,无视正在排队等待加锁的进程,在自旋过程中,获取到锁便可加锁,类似于插队
      • 极端情况下,很多进程正排队等待加锁,此时有进程刚到,开始自旋加锁,如果成功,该进程便插队成功加锁。如果此时不断有进程自旋加锁,则在排队的进程将长时间无法获取到锁
      • 解决:锁添加饥饿状态,该状态下不允许自旋
  • 结论

    • 一般认为普通模式会有更好的性能,因为即使有等待的协程,新的协程可以连续获取到锁
    • 饥饿模式能够防止等待协程长时间获取不到锁。

读写锁

读写锁包含了两种锁:读锁、写锁,因此设计中两种锁的权重可能有下列三种场景:
- 读优先:读任务占有锁时,后续的读任务可以立即获得锁;这种设计可以提高并发性能(后来的读任务不需要等待),但如果读任务太多,会造成写任务一直处于等待中,造成写饥饿现象
- 写优先:指如果有写任务在等待锁,会阻塞后来的读任务获取锁。保证了写任务不会被持续的读进程阻塞,但如果写任务过多,又会导致读任务一直被阻塞,造成读任务饿死。
- 读写权重一致:读写锁的优先级一样,即普通的Mutex
Golang的读写实现中,采用了读优先、写优先交替策略:
  - 在读任务执行过程中,对于接收到的写任务、读任务,采取写优先策略,阻塞接收到的读任务,让写任务在读过程结束后优先执行
  - 在写任务执行过程中,对于接收到的写任务、读任务,采取读优先策略,阻塞接收到的写任务,让读任务在写过程结束后优先执行
  - 使用交替机制,确保不会因为读写任何一方任务过多,造成另一方的任务无法执行
  • 底层结构
type RWMutex struct {
 w           Mutex  // held if there are pending writers
 writerSem   uint32 // semaphore for writers to wait for completing readers
 readerSem   uint32 // semaphore for readers to wait for completing writers
 readerCount int32  // number of pending readers
 readerWait  int32  // number of departing readers
}

w:复用互斥锁提供的能力
writerSem:写操作goroutine阻塞等待信号量,当阻塞写操作的读操作goroutine释放读锁时,通过该信号量通知阻塞的写操作的goroutine
readerSem:读操作goroutine阻塞等待信号量,当写操作goroutine释放写锁时,通过该信号量通知阻塞的读操作的goroutine
redaerCount:当前正在执行的读操作goroutine数量
readerWait:当写操作被阻塞时等待的读操作goroutine个数
  • 获取读锁:

    • 获取读锁时,先将读计数器 readerCount 增1,表示增加一个读任务
    • 当readerCount值为负时,表示前面存在等待处理写任务或有写任务正在处理,此时阻塞新接收到的读任务,等待信号量通知
  • 释放读锁:

    • 释放读锁时,先将读计数器 readerCount 减一,表示完成一个读任务
    • 如果 readerCount 为负,则存在需要优先处理的写任务,进入慢路径
    • 首先检测读计数器的临界区,防止RUnlock调用出错(上锁一次、解锁多次)
    • 因为此时存在写任务,readerWait已被写任务赋值,将该值减一,表示写任务执行前要处理的读任务完成一个
    • 如果readerWait为0,则表示写任务执行之前的所有读任务都已完成,释放写信号量,执行等待处理的写任务
  • 获取写锁:

    • 获取写锁时,先抢占互斥锁;因为当存在多个写任务时,同一时间仅会处理一个
    • 反转readerCount的值为负,同时计算收到写任务时的读任务数量
    • 当读任务数量>0时,表示存在正在处理的读任务,将该值累加给readerWait,表示执行接收到的写任务时需要执行多少任务
    • 当readWait > 0,表示有任务要执行,因为通过信号量将写任务阻塞
  • 释放写锁:

    • 释放写锁时,先将readerCount反转为正值表示写任务执行完成,并计算读任务的数量;在释放写锁期间如果有新到的并发读任务,因为readerCount>=0,可以立即获取读锁执行
    • 释放r次读信号量,将在写任务期间被阻塞的读任务唤醒执行
    • 释放Mutex互斥锁

在这里插入图片描述

  • 总结
    • 读写锁提供四种操作:读上锁,读解锁,写上锁,写解锁;加锁规则是读读共享,写写互斥,读写互斥,写读互斥
    • 读写锁中的读锁是一定要存在的,其目的是也是为了规避原子性问题,只有写锁没有读锁的情况下会导致我们读取到中间值
    • Go语言的读写锁在设计上也避免了写锁饥饿

http://www.kler.cn/news/288984.html

相关文章:

  • k8s-使用Network Policies实现网络隔离
  • (二)、软硬件全开源智能手表,可全面高精度采集生命体征数据,进行健康检测。(HealthyPi Move)
  • 【Java中的三元运算符】
  • 书法图片自动扣字的批处理
  • leecode 31.下一个排列(Golang)
  • 深度学习100问27:什么是截断的BPTT
  • mysql的组从复制
  • 检测文件解析漏洞的工具
  • 技术Leader在训练团队思考力中的核心职责
  • MySQL常用的查询优化分析方法有哪些?
  • 【Qt】 QComboBox | QSpinBox
  • 【qt】qss使用
  • 钢铁百科:A633GrE钢板材质、A633GrE力学性能、A633GrE执行标准
  • JAVA - 关于防重复提交探讨
  • uniapp scroll-view滚动触底加载 height高度自适应
  • centos7 安装python3.12.5
  • 【链表】环形链表
  • Linux-centos7目录结构
  • C++入门基础知识45——【关于C++ 函数】定义函数、函数声明
  • 【网络安全】服务基础第一阶段——第六节:Windows系统管理基础---- DNS部署与安全
  • 【WPF动画】
  • kubeadm部署 Kubernetes(k8s) 高可用集群【V1.20 】
  • 智能创作与优化新时代:【ChatGPT-4o】在【数学建模】、【AI绘画】、【海报设计】与【论文优化】中的创新应用
  • 深度学习100问13:什么是二分类问题
  • 项目实战 ---- 商用落地视频搜索系统(5)---service层核心
  • Python进阶08-爬虫
  • 前端 数值列 禁止输入多个小数点
  • 按图搜索与精准营销:深度剖析拍立淘API用户画像构建
  • AlphaGo围棋模型——基于python语言
  • 交叉编译 gdb