You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言RWMutex非明显死锁场景原因解析请求

分析这段Go代码中大多时候出现死锁的原因

咱们来拆解这段代码里大多时候会触发死锁的核心原因,主要是sync.RWMutex的读写锁优先级机制,再加上调度器的执行时机配合导致的:

先梳理代码的关键执行步骤

  1. 主goroutine初始化一个sync.RWMutex,然后启动一个子goroutine。
  2. 子goroutine首先调用m.RLock()获取读锁,接着调用runtime.Gosched()主动让出CPU。
  3. 主goroutine同样调用runtime.Gosched()让出CPU,之后尝试调用m.Lock()获取写锁。
  4. 子goroutine后续尝试第二次m.RLock(),然后执行两次解锁操作。

死锁触发的核心逻辑

这里的死锁根源在于Go的RWMutex写锁优先的设计:当有goroutine在等待获取写锁时,新的读锁请求会被阻塞,不能插队获取读锁(这个机制是为了避免写锁饥饿,防止一直有读锁抢占导致写锁永远得不到执行)。

咱们一步步看死锁是怎么形成的:

  • 子goroutine第一次调用m.RLock()成功,此时读锁的持有计数为1。
  • 子goroutine调用runtime.Gosched()主动让出CPU,调度器大概率会切换到主goroutine执行。
  • 主goroutine开始执行m.Lock(),尝试获取写锁。但此时子goroutine还持有一个读锁,写锁无法立即获取,于是主goroutine进入写锁等待队列。此时RWMutex会触发写优先机制:所有后续的读锁请求都会被阻塞。
  • 调度器再次切换回子goroutine,子goroutine尝试第二次m.RLock()。但因为已经有写锁在等待了,这次读锁请求直接被阻塞,无法成功获取。
  • 现在进入循环等待的死锁状态:
    • 子goroutine持有1个读锁,但卡在获取第二个读锁的步骤,永远不会执行后续的RUnlock()来释放已持有的读锁。
    • 主goroutine等待写锁,但写锁需要所有读锁都释放才能获取,而子goroutine根本无法走到释放锁的步骤。

为什么是“大多时候”发生死锁?

死锁不是100%触发的原因在于Go调度器的执行顺序不是绝对的:如果子goroutine在第一次RLock()之后,调用runtime.Gosched()但调度器没有立即切换到主goroutine,而是让子goroutine继续执行,那么子goroutine会顺利完成两次RLock()、两次RUnlock(),释放所有读锁,之后主goroutine的Lock()就能正常获取并执行,不会触发死锁。但在大多数情况下,Gosched()会让调度器切换到其他就绪的goroutine(也就是主goroutine),所以死锁大概率会发生。

额外补充:如果去掉中间的runtime.Gosched()会怎样?

如果子goroutine里的两次RLock()是连续执行的,没有中间的Gosched(),那么两次读锁都会顺利获取(读锁支持多个goroutine持有,同一个goroutine也可以多次获取读锁),之后释放所有读锁,主goroutine的写锁也能正常执行,不会有任何问题。问题就出在中间让出CPU的操作,让主goroutine有机会先发起写锁请求,触发了RWMutex的写优先阻塞逻辑。

内容的提问来源于stack exchange,提问作者Anton Litvinov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 06:42:08