Go锁 详解

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

类型

  • 互斥锁(mutex)

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

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

互斥锁

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

state的不同位分别表示了不同的状态,使用最小的内存来表示更多的意义

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

  • 释放锁

  • 正常模式(默认)

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

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

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

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

读写锁

go 复制代码
读写锁包含了两种锁:读锁、写锁,因此设计中两种锁的权重可能有下列三种场景:
- 读优先:读任务占有锁时,后续的读任务可以立即获得锁;这种设计可以提高并发性能(后来的读任务不需要等待),但如果读任务太多,会造成写任务一直处于等待中,造成写饥饿现象
- 写优先:指如果有写任务在等待锁,会阻塞后来的读任务获取锁。保证了写任务不会被持续的读进程阻塞,但如果写任务过多,又会导致读任务一直被阻塞,造成读任务饿死。
- 读写权重一致:读写锁的优先级一样,即普通的Mutex
Golang的读写实现中,采用了读优先、写优先交替策略:
  - 在读任务执行过程中,对于接收到的写任务、读任务,采取写优先策略,阻塞接收到的读任务,让写任务在读过程结束后优先执行
  - 在写任务执行过程中,对于接收到的写任务、读任务,采取读优先策略,阻塞接收到的写任务,让读任务在写过程结束后优先执行
  - 使用交替机制,确保不会因为读写任何一方任务过多,造成另一方的任务无法执行
  • 底层结构
go 复制代码
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语言的读写锁在设计上也避免了写锁饥饿
相关推荐
Pandaconda6 分钟前
【Golang 面试题】每日 3 题(三十九)
开发语言·经验分享·笔记·后端·面试·golang·go
加油,旭杏10 分钟前
【go语言】变量和常量
服务器·开发语言·golang
行路见知10 分钟前
3.3 Go 返回值详解
开发语言·golang
编程小筑40 分钟前
R语言的编程范式
开发语言·后端·golang
技术的探险家43 分钟前
Elixir语言的文件操作
开发语言·后端·golang
Ai 编码助手1 小时前
Golang 中强大的重试机制,解决瞬态错误
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的区块链
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的循环实现
开发语言·后端·golang
梁雨珈2 小时前
Lisp语言的物联网
开发语言·后端·golang
邓熙榆3 小时前
Logo语言的网络编程
开发语言·后端·golang