当前位置:首页 > 文章列表 > Golang > Go问答 > Go RWMutex 为什么读锁会阻塞新读者

Go RWMutex 为什么读锁会阻塞新读者

来源:17golang原创 2026-10-26 19:27:22 0浏览 收藏

Go 的 RWMutex 允许多个读者同时持有读锁,但不保证新的 RLock 永远立即成功。当已有读者持锁、一个写者调用 Lock 等待时,后续读者会被挡住,直到该写者获得并释放锁。这让写者有机会完成更新,避免源源不断的读请求让它一直等待。

所以,读锁本身并不是互相排斥的写锁。出现“老读者还在读,新读者却进不来”时,要同时找等待中的写者和迟迟不释放的老读者。下面用配置中心的假设场景讨论瓶颈,不虚构线上吞吐数据或已经完成的性能测试。

Go 官方地址:https://go.dev/

规模背景:读请求很多,配置更新很少

假设服务把租户配置放在一个 map 中,读接口用 RLock,刷新接口用 Lock。平时读操作短,多个请求可以共享读锁;某个查询却在持锁期间发送网络请求或渲染大报告。只要没有写者,它的影响可能暂时不明显。

当刷新任务开始等待写锁,后续读请求也开始等待。此时即便写者的实际更新只有几行赋值,读请求延迟仍可能受那个长时间持锁的读者影响。只看读写比例,容易漏掉这个触发条件。

原架构瓶颈:等待写者改变了新读者的准入

把关系拆成三个角色更容易理解:已有读者 A 正持有 RLock;写者 W 在 Lock 中等待;后来读者 B 申请 RLock。A 不会因为 W 等待就被强制释放,但 B 不能一直插队。A 释放后,W 才有机会拿到独占锁,更新结束释放后,读者才能继续竞争。

这不是严格的业务 FIFO 队列,也不是给每个请求承诺最大等待时长。不要从这条规则推导出所有 goroutine 的完整执行顺序。排查时应关注锁持有范围与调用关系,而不是凭日志行先后猜测调度。

已有读者、等待写者与新读者的锁关系结构图
图1:读写者关系结构图。已有读者持锁、写者等待、新读者申请属于同一个 RWMutex 的关系;图是静态说明,不是执行时间轴或实际阻塞截图。

改造读路径:锁内复制配置,锁外执行慢操作

对于只含值字段的配置,我会优先让读方法返回一个独立值,把锁范围压到查询和复制。网络请求、模板生成、日志编码等工作使用这个局部值,避免把不确定耗时带进共享锁。

package main

import (
    "fmt"
    "sync"
)

type Config struct {
    Endpoint string
    Enabled  bool
}

type Store struct {
    mu    sync.RWMutex
    items map[string]Config
}

func (s *Store) Get(key string) (Config, bool) {
    s.mu.RLock()
    cfg, ok := s.items[key]
    s.mu.RUnlock() // 只保护 map 读取与值复制,后续处理在锁外。
    return cfg, ok
}

func (s *Store) Put(key string, cfg Config) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.items == nil {
        s.items = make(map[string]Config)
    }
    s.items[key] = cfg
}

func describe(cfg Config) string {
    // 这里仅做文本处理;真实耗时操作也应使用锁外的局部配置。
    return fmt.Sprintf("endpoint=%s enabled=%t", cfg.Endpoint, cfg.Enabled)
}

func main() {
    var store Store
    store.Put("tenant-a", Config{Endpoint: "https://example.com/", Enabled: true})
    cfg, ok := store.Get("tenant-a")
    if !ok {
        fmt.Println("配置不存在")
        return
    }
    fmt.Println(describe(cfg))
}

这是完整的原创示例,没有在本文生成过程中运行。它演示安全的 map 访问和锁范围,不是并发性能基准。Get 返回的 Config 包含字符串和布尔值,适合按值独立使用;若改成包含 map、切片或可变指针,单纯复制外层结构体仍可能共享内部对象,需要深复制、不可变约定或额外同步。

Store 与配置副本及锁外处理的归属结构图
图2:配置归属结构图。Store 持有 map 与锁,Get 产生局部 Config,describe 使用局部值;边界说明共享数据与独立值的区别,不表示运行结果。

关键取舍:别用递归读锁绕过等待

一个危险修补是认为“我已经持有读锁,再拿一次也没关系”。如果外层函数持有 RLock,又调用会获取同一把 RLock 的 Get,恰好有写者等待时,内层读锁可能阻塞,外层读锁却要等内层返回才释放,构成相互等待。sync 文档明确不允许依赖递归读锁。

应让公共方法自己加锁,并避免在持锁时调用它;确实需要组合多个字段一致读取时,把组合操作集中到一个加锁方法中,或使用清楚命名、仅限内部且要求调用方已持锁的辅助函数。不能把辅助函数暴露给不知道锁约定的调用方。

同样不能把 RLock 原地升级成 Lock。需要修改时,通常应结束读锁阶段、获取写锁并重新检查条件;两阶段之间数据可能改变,不能拿旧判断直接写入。解锁再重锁也不是原子升级。

上线后看什么:验证等待缩短,而非只看读写比例

实际改造应在相同工作负载下观察锁等待、临界区耗时和请求尾延迟,同时记录配置刷新完成时间。可以在受控环境用阻塞或互斥分析定位热点,但不能因为大多数请求是读就直接断言 RWMutex 比 Mutex 快。

还要检查一致性要求:锁外处理拿到的是某个读取时刻的配置值,不保证处理结束时仍然是最新版本。如果业务必须使用同一版本完成一组工作,可以显式带版本标识或设计快照,而不是把读锁扩展到整个外部调用。

现象优先检查修复方向
新读者突然一起等待是否有等待写者及长读临界区缩短所有持锁路径
调用 Get 后不返回上层是否已经持有同一把锁消除嵌套获取
返回配置后仍有数据竞争是否共享 map、切片或指针字段明确复制深度与可变性
更新判断失效释放读锁后是否重新检查条件在写锁内重查

后续改进:按共享边界选择结构

如果不同租户之间相互独立,可以评估分片锁,但跨分片操作会增加锁顺序和一致性成本。若配置更新很少、读取要求稳定快照,可以进一步评估不可变快照与原子发布;代价是复制成本、内存占用和发布规则。先缩短临界区,往往比直接替换所有同步结构更容易判断收益。

Store 中包含 RWMutex,首次使用后不能复制。方法采用指针接收者,调用方也应通过指针传递已使用的 Store。锁只保护参与同一约定的访问,不能保护绕开 Store 直接读写 items 的代码。

相关问题

只有读者、没有写者时,读锁会互斥吗?

RWMutex 支持多个读者共同持有锁。本文讨论的新读者阻塞,核心条件是写者正在申请独占访问,或已经持有写锁。

用 TryRLock 循环重试会更快吗?

忙循环不会缩短老读者的持锁时间,还会消耗 CPU。先定位长临界区和嵌套调用,再决定是否需要业务降级,不要用重试掩盖同步设计问题。

RUnlock 会释放所有读者吗?

不会。每次 RUnlock 只撤销一次读锁获取;其他并发读者仍需完成各自的释放。

参考:sync.RWMutex 官方约定、RLock 与递归读锁限制。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go atomic.Value 为什么第一次 Store 决定后续类型Go atomic.Value 为什么第一次 Store 决定后续类型
上一篇
Go atomic.Value 为什么第一次 Store 决定后续类型
Go channel 为什么发送到 nil channel 会永久阻塞
下一篇
Go channel 为什么发送到 nil channel 会永久阻塞
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    717次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    724次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    800次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    760次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    593次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码