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问答 | 24分钟前 | 并发 · Go问答 · atomic.Value · Go atomic.Value 并发初始化 配置快照
- Go atomic.Value 首次 Store 与 Load 前初始化有什么区别
- 314浏览 收藏
-
- Golang · Go问答 | 36分钟前 | go · race detector · 并发排查 ·
- Go race detector 没报错但数据仍不一致该查什么
- 479浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go Mutex 复制后为什么解锁异常
- 189浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go sync.WaitGroup 如何用计数快照避免 Add 竞态
- 441浏览 收藏
-
- Golang · Go问答 | 2小时前 | channel · goroutine · go · Context · sync.WaitGroup ·
- Go 如何让多个生产者在不抢 close 权限的情况下退出
- 497浏览 收藏
-
- Golang · Go问答 | 3小时前 | go · time.Ticker · 周期调度 · Go 定时任务 time.Ticker 任务漂移
- Go ticker 长时间运行后任务越来越漂移怎么办
- 247浏览 收藏
-
- Golang · Go问答 | 3小时前 | go · time.Time · 时间处理 · Go time.Time 时间比较 Time.Equal
- Go time.Time 用 == 比较同一时刻为什么失败
- 298浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 102次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 16次使用
-
- OpenCompass
- OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
- 29次使用
-
- AGI-Eval
- AGI-Eval是由上海交大等高校联合发布的大模型评测社区,提供公正透明的LLM能力榜单、多领域评测集及Data Studio数据服务,助力AI模型性能评估与NLP科研开发。
- 17次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 257次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go select 用 time.After 做超时有什么资源代价
- 2026-09-10 501浏览
-
- Go 取 range 变量地址为什么得到重复指针
- 2026-09-07 501浏览
-
- Go net.Conn 写入超时为何仍会卡住:SetWriteDeadline、部分写入与连接复用检查
- 2026-08-30 501浏览

