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

Go语言读写锁(RWMutex)底层原理详解

Go语言读写锁(RWMutex)底层原理详解

概述

Go语言的sync.RWMutex是一种读写锁,允许多个读操作同时进行,但写操作是互斥的。这种锁机制在读多写少的场景下能显著提高并发性能。底层通过互斥锁和原子计数器实现复杂的并发控制。

核心数据结构

type rwmutex struct {rLock      mutex    // 保护读操作相关的数据(readers, writer)readers    muintptr // 等待读操作的队列wLock      mutex    // 控制写操作的互斥锁writer     muintptr // 等待完成读操作的写操作readerCount atomic.Int32 // 当前有多少个正在进行的读操作readerWait  atomic.Int32 // 写操作需要等待的读操作的数量readerPass  int32     // 读通过计数
}

字段说明

  • rLock: 保护读操作相关的数据(readers, writer队列)
  • readers: 等待读操作的队列(muintptr链表结构)
  • wLock: 控制写操作的互斥锁,确保写操作互斥
  • writer: 等待完成读操作的写操作指针
  • readerCount: 当前有多少个正在进行的读操作,关键设计:负数表示有写操作竞争
  • readerWait: 写操作需要等待的读操作的数量
  • readerPass: 写操作完成后允许通过的读操作数

状态转换机制

1. rlock() 获取读锁流程

rlock()操作流程:

func (rw *rwmutex) rlock() {// 增加当前读操作的计数,如果 +1 还小于0,说明目前有写锁竞争if rw.readerCount.Add(1) < 0 {// 如果有写操作等待,则当前读操作需要挂起,排队等待写操作完成systemstack(func() {// 获取rLock锁lock(&rw.rLock)if rw.readerPass > 0 {// 如果写操作已经完成,跳过当前读操作rw.readerPass -= 1unlock(&rw.rLock)} else {// 将当前读操作加入等待队列,等待写操作释放锁m := getg().mm.schedlink = rw.readersrw.readers.set(m)unlock(&rw.rLock)// 当前读操作挂起,等待写操作唤醒notesleep(&m.park)noteclear(&m.park)}})}
}

关键点:

  • 原子操作递增readerCount
  • 如果结果为负数,说明有写者在等待,当前读者需要阻塞
  • 这种设计实现了写者优先的策略

2. runlock() 解锁读锁流程

runlock()操作流程:

func (rw *rwmutex) runlock() {// 如果r小于0说明此时有writer竞争if r := rw.readerCount.Add(-1); r < 0 {if r+1 == 0 || r+1 == -rwmutexMaxReaders {throw("runlock of unlocked rwmutex")}// 将等待reader的计数减1,如果值==0,说明读等待都处理完了// 此时需要唤醒写等待if rw.readerWait.Add(-1) == 0 {lock(&rw.rLock)w := rw.writer.ptr()if w != nil {notewakeup(&w.park)}unlock(&rw.rLock)}}
}

关键点:

  • 原子操作递减readerCount
  • 如果递减后为负数,说明有写者在等待
  • readerWait减到0时,唤醒等待的写者

3. lock() 获取写锁流程

lock()操作流程:

const rwmutexMaxReaders = 1 << 30func (rw *rwmutex) lock() {// 获取写互斥锁,确保只有一个写操作可以执行lock(&rw.wLock)m := getg().m// 将readerCount - 最大读锁数,肯定得到一个负数// 不会丢失当前正在进行的读操作数量,又可以将值设置为负数r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders// 获取读锁lock(&rw.rLock)// 如果有读操作正在进行,写操作需要等待它们完成if r != 0 && rw.readerWait.Add(r) != 0 {// 等待读操作完成后才能执行写操作systemstack(func() {// 设置当前写操作为等待状态rw.writer.set(m)unlock(&rw.rLock)// 当前写操作挂起,等待读操作唤醒notesleep(&m.park)noteclear(&m.park)})} else {unlock(&rw.rLock)}
}

关键点:

  • 先获取基础互斥锁w
  • readerCount减去一个很大的数(rwmutexMaxReaders),使其变为负数
  • 等待现有读者完成(通过readerWait计数)

4. unlock() 解锁写锁流程

unlock()操作流程:

func (rw *rwmutex) unlock() {// 将readerCount复原,表示当前写操作已经完成r := rw.readerCount.Add(rwmutexMaxReaders)if r >= rwmutexMaxReaders {// 如果没有锁被持有,抛出异常throw("unlock of unlocked rwmutex")}// 获取rLock锁,操作读者队列lock(&rw.rLock)// 遍历并唤醒所有在读者队列中的等待操作for rw.readers.ptr() != nil {reader := rw.readers.ptr()rw.readers = reader.schedlinkreader.schedlink.set(nil)notewakeup(&reader.park)r -= 1}unlock(&rw.rLock)// 释放写锁,允许其他写操作进行unlock(&rw.wLock)
}

关键点:

  • readerCount恢复为正数
  • 唤醒所有等待的读者
  • 最后释放基础互斥锁

优先级策略

写者优先机制

Go的RWMutex实现了写者优先的策略,这是通过以下机制实现的:

  1. 负数标记:当写者到来时,将readerCount设置为负数
  2. 新读者阻塞:新来的读者发现readerCount为负数时会阻塞
  3. 写者等待:写者会等待现有读者完成,但不会让新读者"插队"

避免饥饿

这种设计避免了写者饥饿问题:

  • 如果不断有新读者到来,写者可能会永远等待
  • 通过负数标记,新读者会被阻塞,确保写者最终能获得锁

并发控制流程

读操作并发

graph TDA[Reader1请求读锁] --> B[readerCount++]C[Reader2请求读锁] --> D[readerCount++]B --> E[Reader1获得读锁]D --> F[Reader2获得读锁]E --> G[Reader1释放读锁]F --> H[Reader2释放读锁]G --> I[readerCount--]H --> J[readerCount--]

读写互斥

graph TDA[Reader持有读锁] --> B[Writer请求写锁]B --> C[readerCount设为负数]C --> D[Writer等待]A --> E[Reader释放读锁]E --> F[readerCount--]F --> G[检查readerWait]G --> H[唤醒Writer]H --> I[Writer获得写锁]

性能特点

优点

  1. 高并发读:多个读操作可以同时进行
  2. 写者优先:避免写者饥饿
  3. 公平性:在读写竞争中保持相对公平

缺点

  1. 写操作开销:写操作需要等待所有读者完成
  2. 内存开销:需要维护多个计数器和信号量
  3. 复杂度:实现逻辑相对复杂

使用场景

适合场景

  • 读多写少:如配置文件读取、缓存查询
  • 读操作耗时短:快速读取,避免长时间持有锁
  • 写操作不频繁:偶尔的更新操作

不适合场景

  • 写操作频繁:会导致读者频繁阻塞
  • 读操作耗时:长时间持有读锁会影响写操作
  • 需要严格公平:某些场景可能需要更复杂的公平策略

总结

Go语言的RWMutex通过巧妙的设计实现了高效的读写锁机制:

  1. 原子操作:使用原子操作保证计数器的准确性
  2. 信号量机制:通过信号量实现等待/唤醒机制
  3. 负数标记:用负数表示写者等待状态
  4. 写者优先:避免写者饥饿问题

这种设计在读多写少的场景下能显著提高并发性能,是Go并发编程中的重要工具。

注意:在实际使用中,要避免在持有锁时进行耗时操作,以免影响其他goroutine的执行。同时,要确保锁的正确释放,避免死锁。

http://www.wxhsa.cn/company.asp?id=1656

相关文章:

  • 【GitHub每日速递】无需提示词!Nano Bananary香蕉超市:AI绘画玩法多到停不下来
  • 小题狂练 (J)
  • Drift数据库开发实战:类型安全的SQLite解决方案
  • DELPHI FireDAC连接EXCEL文件
  • 读人形机器人09教育行业
  • PHP判断字符串是否包含中文
  • 当我们红尘作伴,活得潇潇洒洒
  • 诡异的mysql8的问题
  • 二叉树理论
  • 支付中心的熔断降级要怎么做
  • 协议版iM蓝号检测,批量筛选iMessages数据,无痕检测是否开启iMessage服务
  • 栈和队列总结
  • 工业互联网认知实训台-一句话介绍
  • 湾区杯 SilentMiner WP
  • Python-课后题题目-1.1编程世界初探
  • Python-课后题题目-1.2初识python语言
  • node和npm相关的记录
  • 在Spring boot 中使用@master 设置主从数据库
  • 设计模式-装饰器模式 - MaC
  • 【API接口】最新可用河马短剧接口
  • 第 16 章反射(reflection)
  • 自我介绍+软工5问
  • 电容器+动生电动势+自由落体模型
  • 引用(reference)
  • 设计模式-组合模式 - MaC
  • 【推荐】100%开源!大型工业跨平台软件C++源码提供,建模,组态
  • tmux 使用教程
  • 引用类型
  • CF1237C2