读写锁
Go 的 sync.RWMutex 没有“把读锁升级成写锁”的操作。持有 RLock 时直接调用 Lock,当前 goroutine 会等所有读者退出,而它自己还没有调用 RUnlock,于是形成等待环。处理读后写场景时,应先释放读锁,再获取写锁,并在写锁内重新检查一次状态。
RLock和Lock是两种互斥状态,官方语义不支持直接升级。- 正确顺序是:读锁判断、释放读锁、写锁重查;不能把第一次读取的结果直接当成最终结论。
- 是否使用
RWMutex要看读写比例、锁等待时间和实际基准,不是锁名字里有“读”就一定更快。
为什么持有 RLock 时再 Lock 会卡住
RWMutex 允许多个读者同时进入,但同一时刻只能有一个写者。它还要保证写者最终能获得机会:当某个 goroutine 已经等待 Lock 时,后续新的 RLock 会被挡住,直到写者完成。
问题在于“升级”并不是把状态上的一个标记改掉。下面的反例在发现缓存缺失后尝试写入:
func (c *Cache) BrokenGet(key string) string {
c.mu.RLock()
defer c.mu.RUnlock() // 读锁还没释放,函数返回前才会执行
if value, ok := c.items[key]; ok {
return value
}
c.mu.Lock() // 这里等待所有读者退出,其中包括当前 goroutine
defer c.mu.Unlock()
c.items[key] = loadValue(key)
return c.items[key]
}
这里不是“偶尔竞争失败”,而是锁的生命周期自相矛盾:Lock 要等当前读者减少,当前 goroutine 却要等 Lock 返回后才有机会执行 RUnlock。Go 官方文档因此明确写出,RLock 不能升级为 Lock,Lock 也不能降级为 RLock。

读后写场景要用“释放、获取、重查”
常见需求不是“升级锁”,而是先用便宜的读锁查一次,只有缺失时才修改共享数据。此时第一次读取只是线索,不能跨越解锁阶段直接复用。一个可控的缓存写入方式如下:
type Cache struct {
mu sync.RWMutex
items map[string]string
}
func (c *Cache) GetOrLoad(key string, load func(string) string) string {
c.mu.RLock()
value, ok := c.items[key]
c.mu.RUnlock() // 先结束读临界区,不能用 defer 把它拖到写锁之后
if ok {
return value
}
c.mu.Lock()
defer c.mu.Unlock() // 写入和第二次检查必须处在同一个临界区
if value, ok = c.items[key]; ok {
return value // 其他 goroutine 可能已经在空档期完成写入
}
value = load(key) // 回调不要再次调用会获取 c.mu 的方法
if c.items == nil {
c.items = make(map[string]string)
}
c.items[key] = value
return value
}
第二次检查是这段代码的关键。当前 goroutine 释放读锁到重新获取写锁之间,其他 goroutine 可能已经填充了同一个键;不重查就会重复加载,甚至覆盖更新后的值。若 load 很慢,还应把耗时工作移到锁外,并为结果设计版本或单飞协调机制,避免把慢回调放在写锁里。

怎样用指标判断 RWMutex 是否值得保留
读写锁的优势来自读者之间可以并行,而不是来自 API 名称。先记录访问路径的读写比例,再分别观察锁等待时间、临界区耗时和整体吞吐;如果写请求频繁、临界区很短,Mutex 可能更简单也更快。
| 观察项 | 怎么记录 | 它能回答什么 |
|---|---|---|
| 读写比例 | 分别统计 RLock 路径与 Lock 路径的请求数 | 是否存在足够的并行读机会 |
| 锁等待 | 在进入临界区前后记录等待时长分布 | 瓶颈是竞争还是临界区逻辑 |
| 基准结果 | 固定数据规模,比较 RWMutex 与 Mutex 的 ns/op、allocs/op | 改锁是否真的改善当前负载 |
# 只运行基准,不运行普通测试;基准函数应在同一数据规模下比较两种锁 go test -run '^$' -bench='BenchmarkCache' -benchmem ./... # 多次运行降低单次调度噪声,结果仍要结合业务读写比例解释 go test -run '^$' -bench='BenchmarkCache' -benchmem -count=5 ./...
不要把一次本地 ns/op 直接当成线上结论。基准中要固定 goroutine 数、键分布、命中率和回调耗时;线上则继续看 p95/p99 等等待分位数。这样才能区分“读者并行带来的收益”和“锁内工作变少带来的收益”。
这几个边界容易把问题带回原点
- 递归读锁:看似只是再次读取,但待写者会阻塞新的读者,递归调用可能永远等不到返回。
- 锁内回调:回调如果再次访问同一对象并获取同一把锁,会形成隐式自等待;把回调契约写清楚。
- 跨 goroutine 解锁:
RWMutex不绑定 goroutine,但跨协程交接会让生命周期难以审查,除非所有权边界非常明确。 - 复制锁对象:
RWMutex第一次使用后不能复制;包含它的结构体也要避免值拷贝。
相关问题
释放 RLock 后还需要重新读取吗?
如果后续要写共享状态,需要重新检查。解锁期间状态可能变化,第一次读到的结果只代表当时的观察。
能不能先 RUnlock,再马上 Lock?
可以,这是避免等待环的必要步骤,但它不是原子升级;在两次调用之间,其他 goroutine 可能先完成写入。
读多写少就一定该用 RWMutex 吗?
不一定。只有在读者并行收益能抵消更复杂的状态维护和竞争成本时才值得采用,最终用固定负载基准确认。
-
- Golang · Go问答 | 3分钟前 | 标准库 · 并发编程 · Go问答 · Go 对象生命周期 sync/atomic atomic.Pointer 并发读取 Pointer.Load
- 原子指针
- 278浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 982次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 986次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1105次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1075次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 916次使用
-
- GosyncWaitGroup使用深入理解
- 2022-12-22 122浏览
-
- Go语言读写锁RWMutex的源码分析
- 2022-12-31 319浏览
-
- Go语言同步等待组sync.WaitGroup结构体对象方法详解
- 2022-12-30 201浏览
-
- SingleFlight模式的Go并发编程学习
- 2023-01-01 285浏览
-
- Go并发编程中sync/errGroup的使用
- 2023-02-24 158浏览

并发计数复用
