Go RWMutex 读锁升级为写锁为什么会死锁
遇到 Go 里的“读锁升级死锁”,先看调用顺序:如果同一个 goroutine 已经执行了 RLock(),又在没有执行 RUnlock() 的情况下调用 Lock(),它会一直等下去。原因不是写锁性能慢,而是 sync.RWMutex 没有把读锁原子升级为写锁的语义;写锁必须等所有读锁离开,当前 goroutine 却正占着其中一个读锁。
安全原则是:读阶段只观察并保存快照,先释放读锁;需要修改时再申请写锁,并在写锁内重新检查条件。若业务要求“检查和更新”不可被插入,就把这段状态放进单独的 Mutex 或专门的状态锁中。
RLock后直接Lock不是升级,而是互相等待。- 释放读锁后重取写锁时,必须接受状态可能已经变化,并重新确认条件。
- 锁的复制、漏解锁和把外部调用放进临界区,都会把问题扩大。
为什么 RLock 后再 Lock 会卡住
下面的最小示例只用来说明等待关系,不需要在生产代码中保留。Lock 的调用点永远等不到返回,所以后面的 Unlock 也永远不会执行。
package main
import "sync"
func upgrade(mu *sync.RWMutex) {
mu.RLock()
defer mu.RUnlock() // 读阶段原本会在函数返回时释放读锁
mu.Lock() // 错误:写锁等待所有读锁,包括当前持有的读锁
defer mu.Unlock() // 只有 Lock 返回后才有机会登记写锁释放
}
把现象拆成两个条件就清楚了:RLock 允许多个读者同时进入;Lock 需要排除读者。当前 goroutine 不能“暂时保留读锁并把自己算作例外”,也不能靠再次调用 RLock 来完成升级。
| 调用状态 | 锁的实际要求 | 结果 |
|---|---|---|
| 已经持有 RLock | 当前读者仍在锁内 | 可以继续读取,但不能升级 |
| 调用 Lock | 等待全部读锁释放 | 当前 goroutine 被挂起 |
| 准备 RUnlock | 需要先走过 Lock 返回点 | 形成自等待 |

修复:释放读锁后重新获取写锁
最常见的改法是把读写阶段分开。读锁内只取出足够判断的快照,离开读锁后再申请写锁;由于等待期间别的 goroutine 可能已经完成更新,写锁内必须再次检查。
func (c *Cache) PutIfMissing(key, value string) bool {
c.mu.RLock()
_, exists := c.items[key] // 读锁只负责读取共享 map
c.mu.RUnlock()
if exists {
return false
}
c.mu.Lock()
defer c.mu.Unlock() // 写路径统一负责释放写锁
if _, exists = c.items[key]; exists {
return false // 重取写锁后复核,避免覆盖先到的结果
}
c.items[key] = value
return true
}
这段写法解决的是死锁,不等于把整个“检查—写入”变成了无竞争事务。真正的原子性由写锁内的第二次检查保证。若检查需要调用数据库、网络或回调,不要把这些慢操作放进锁里;先取得结果,再用短临界区提交状态。

什么时候应该改用 Mutex 或单独状态锁
如果业务真正需要的是“判断状态并立刻更新”,读锁带来的并发读取收益可能不值得承担两阶段重检。此时用普通 sync.Mutex 把状态转换包起来,通常更容易证明正确。
func (c *Cache) PutIfMissing(key, value string) bool {
c.mu.Lock()
defer c.mu.Unlock() // 检查与写入共享同一把互斥锁
if _, exists := c.items[key]; exists {
return false
}
c.items[key] = value
return true
}
还可以把“高频只读数据”和“需要原子变更的状态”拆开:前者由 RWMutex 保护,后者由独立 Mutex 保护。关键不是锁的名字,而是让一个不变量只由一条清晰的锁边界维护。
排查时别漏掉这几个锁坑
- defer 位置太晚:
defer RUnlock()只适合保护整个读临界区;如果后面还要申请写锁,应在申请写锁前显式释放。 - 复制含锁的结构体:
RWMutex不应在首次使用后复制,缓存对象应通过指针或构造后固定的实例共享。 - 错误的解锁配对:
RLock对应RUnlock,Lock对应Unlock,混用会触发运行时错误。 - 把外部工作放进临界区:网络、磁盘和回调时间不可控,会让等待被误判成升级死锁。
修复后可以给锁竞争路径加一个有限超时的测试 harness,再用 go test -race 检查数据竞争。竞态检测器不能证明没有死锁,但能发现另一类“先读后写未受保护”的问题;死锁判断仍要靠清晰的锁顺序和可退出测试。
相关问题
RWMutex 能不能在 RLock 后先 RUnlock 再 Lock?
可以,这是常见的分阶段写法,但两次调用之间状态可能变化,因此写锁内必须重新检查条件。
多个 goroutine 都想从读锁升级会怎样?
它们都可能持有读锁并等待写锁,而写锁又等待读锁清空,形成整体停顿。不要把升级逻辑放进共享的读路径。
读多写少就一定应该用 RWMutex 吗?
不一定。若临界区很短、状态转换很多或证明成本较高,普通 Mutex 往往更直接;先按不变量和实际竞争选择。
结论
Go sync.RWMutex 的读锁和写锁是两种互斥语义,不提供升级通道。看到“RLock 后 Lock 卡住”,先检查是否让当前读者等待自己退出;然后把读快照、释放读锁、写锁复核和状态更新分成清楚的边界。需要原子检查更新时,直接使用 Mutex 或独立状态锁,通常比模拟升级更稳。
MySQL 死锁日志看不懂时先找哪两条事务
- 上一篇
- MySQL 死锁日志看不懂时先找哪两条事务
- 下一篇
- Redis LRU 和 LFU 淘汰策略如何按访问特征选择
-
- Golang · Go问答 | 1小时前 | go · 资源管理 · 资源释放 runtime.SetFinalizer runtime.KeepAlive Go finalizer
- Go finalizer 不适合作为资源释放机制的替代设计
- 389浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go buildid 与 -s -w 裁剪选项的发布取舍
- 308浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go IPv4 mapped IPv6 白名单未命中的地址归一化
- 146浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go DNS 在本机与容器不同的 Resolver 配置
- 324浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go ldflags 未写入版本信息的链接检查
- 496浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go generate 未执行指令的注释与目录检查
- 501浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 自动工具链切换的 GOTOOLCHAIN 判断
- 330浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · use go.work Go workspace
- Go workspace 未使用本地模块的 use 配置
- 479浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go replace 仅本机生效的发布配置
- 304浏览 收藏
-
- Golang · Go问答 | 1小时前 | 标准库 · go · Go问答 · Go 资源文件 filepath.EvalSymlinks os.Executable 程序路径
- Go os.Executable 返回路径不稳定时怎样定位程序文件
- 403浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 748次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 745次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 831次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 796次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 628次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go generate 未执行指令的注释与目录检查
- 2026-10-28 501浏览
-
- Go RWMutex 写锁等待过久的竞争分析
- 2026-10-28 501浏览
-
- Go os.File.ReadAt 为什么到文件末尾仍返回部分数据
- 2026-10-26 501浏览

