登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go RWMutex 为什么写锁会长期饥饿

来源:17golang原创

时间:2026-10-26 00:29:22 125浏览 收藏

Go 的 sync.RWMutex 当前实现并不会让新读者永远插到写者前面:当某个 goroutine 已经调用 Lock 并等待时,后续 RLock 会被挡住。写锁仍然长期拿不到,通常不是“读请求一直插队”,而是旧读锁没有及时退出、读锁被递归获取、另一个写者正在排队,或者写临界区本身太长。

要点速览
  • pending writer 会阻止新的 RLock,但不能取消已经持有的读锁。
  • 先把等待拆成“等旧读者、等其他写者、等写区结束”三段,再查锁生命周期。
  • 读锁内不要做网络、磁盘或不可控回调;复制出快照后尽快 RUnlock。

RWMutex 的公平性边界到底是什么

官方文档明确说明:如果已经有读者持锁时出现 Lock,并发到达的新 RLock 会等待,直到写者获得并释放锁。这样做是为了让写者最终有机会进入;同时,RWMutex 不支持递归读锁,也不能把读锁升级成写锁。

实现上可以把 readerCount 看成读者入口的状态计数。写者到达后会标记“有 pending writer”,随后新读请求在 readerSem 上等待;已经在读临界区里的 goroutine 仍要自己执行 RUnlock,最后一个离开的读者才会唤醒写者。

Go RWMutex 中 readerCount、已持有读锁、pending writer、readerSem 与新 RLock 的读写边界说明图
图1:RWMutex 读写边界说明图,展示写者到达后新读请求的等待位置;这是静态结构图,不是运行截图。
读写锁出现写锁长期饥饿,本质是Go原生RWMutex的默认调度策略优先放行读锁,当大量读请求持续占住锁区间时,后续抵达的写锁会一直卡在等待队列里,没法抢到执行权限。

写锁等待要拆成三段,而不是只看 RLock 数量

写者调用 Lock 后,第一段可能是在等当前读者退出。哪怕系统此时没有新的读请求,只要一个读者把锁带进慢查询、网络调用或 channel 等待,写者就会持续阻塞。第二段是等待其他写者:多个 Lock 之间仍然需要排队,读写锁并不会让写操作并行。第三段是前一个写者已经进入,但写临界区做了过多工作,后续写者只能继续等。

观察到的等待位置优先怀疑处理方向
读者仍未退出读区太大、漏 RUnlock、递归读锁缩短读区,逐个核对配对关系
写者彼此排队写入频率高或写区太长合并批量更新,减少锁内工作
写者进入后又拖住别人锁内包含 I/O、回调或阻塞等待先准备数据,锁内只提交共享状态
Go RWMutex 从 RLock 到 RUnlock、Lock、writer queue、写临界区和 Unlock 的写锁等待诊断边界图
图2:写锁等待诊断边界图,把读锁生命周期、写者排队和写入边界拆开;这是说明图,不是性能火焰图。

用锁生命周期检查真正的长等待来源

排查时先找所有 RLock,确认每条返回路径都会到达对应的 RUnlock。如果读函数内部又调用同一对象的读方法,递归读锁会在写者已经等待时卡住:内层 RLock 不能越过 pending writer,而外层读锁又没有退出,写者也就等不到最后一个读者离开。

import "sync"

type SnapshotStore struct {
    mu   sync.RWMutex
    data map[string]string
}

func (s *SnapshotStore) Snapshot(key string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock() // 无论命中、未命中还是提前返回,都释放读锁。

    value, ok := s.data[key]
    return value, ok // 只复制必要值,不把读锁带到调用方。
}

func (s *SnapshotStore) Replace(key, value string) {
    // 复杂计算、网络请求应放在 Lock 之前,避免扩大写者等待链。
    s.mu.Lock()
    defer s.mu.Unlock() // 只保护共享 map 的替换动作。
    s.data[key] = value
}

这段写法的重点不是把每个函数都套上 defer,而是让锁的生命周期和函数边界一致。读锁内若必须访问外部系统,先评估是否可以复制需要的数据再释放锁;写锁内若包含回调,要特别防止回调再次访问同一把锁。

怎样判断是饥饿,还是锁的设计边界

可以在写者获取锁前后记录等待时长,同时给读路径记录持锁时长。若写者等待期间仍有一个固定 goroutine 长时间持有读锁,根因就是读区生命周期;若读者已经全部退出但写者仍排队,则继续看其他写者和写区长度。不要用 TryLock 反复轮询来掩盖等待,它通常只会把锁争用变成忙等和丢更新。

  • 先记录等待开始、成功拿锁和释放锁三个时间点。
  • 把读锁内的 I/O、序列化、回调和 channel 等待列出来。
  • 确认没有在持有读锁时再次调用同一对象的读方法。
  • 确认写操作只修改共享状态,批量计算和数据准备在锁外完成。

常见问题

新的 RLock 会不会一直抢在写锁前面?

正常情况下不会。Go 的 RWMutex 在写者等待后会挡住新的读者;真正拖住写者的,通常是已经持有的读锁或其他写者。

把 RLock 改成 Lock 就能解决长期等待吗?

不一定。它可能消除读者并发,但也可能让更多调用方排队;应该先确认读区是否过大、是否漏解锁,再决定是否改变锁粒度。

读锁能不能升级成写锁?

不能安全升级。先持有 RLock 再调用 Lock 可能形成自我等待,应该先释放读锁、重新计算状态,再单独获取写锁并重新检查条件。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>