
Go 的 sync.RWMutex 不支持同一 goroutine 多次获取读锁(RLock),若在已持有读锁的情况下再次调用 RLock,且此时有写锁正在等待,则该 goroutine 会永久阻塞,进而引发全局死锁。
go 的 `sync.rwmutex` 不支持同一 goroutine 多次获取读锁(rlock),若在已持有读锁的情况下再次调用 rlock,且此时有写锁正在等待,则该 goroutine 会永久阻塞,进而引发全局死锁。
sync.RWMutex 是 Go 标准库中实现读写分离的同步原语,其设计遵循 写优先(write-preference) 策略——这是为防止“写饥饿”(writer starvation)而采用的经典方案。根据 Go 官方文档,当 Lock() 被调用但无法立即获取写锁时,它会主动阻塞后续所有新的 RLock() 请求,直到自身成功获得锁并完成操作。这一机制确保了写操作不会被无限期延迟。
问题就出在这里:若某 goroutine 已通过 RLock() 持有一个读锁,随后在未释放前再次调用 RLock(),而此时恰有另一个 goroutine 正在等待 Lock()(即写锁),那么第二次 RLock() 将被阻塞。但由于该 goroutine 仍持有第一次读锁,RUnlock() 永远不会执行,导致读锁计数无法归零;而写锁因“等待中禁止新读锁”的规则继续等待,最终形成双向阻塞闭环——典型的死锁。
以下代码复现了该问题:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.RWMutex
go func() {
mu.RLock()
defer mu.RUnlock() // ← 这行永远不会执行!
fmt.Println("Acquired first RLock")
time.Sleep(100 * time.Millisecond) // 延迟以确保写锁已开始等待
mu.RLock() // ⚠️ 危险:重复 RLock!此时若 Lock 已阻塞,则此处永久挂起
defer mu.RUnlock()
fmt.Println("Acquired second RLock")
}()
time.Sleep(50 * time.Millisecond)
fmt.Println("Attempting write lock...")
mu.Lock()
fmt.Println("Write lock acquired")
mu.Unlock()
fmt.Println("Write lock released")
// 程序将在此 hang 住,无法退出
time.Sleep(2 * time.Second)
fmt.Println("Done") // ← 永远不会打印
}运行后可观察到:程序输出 "Attempting write lock..." 后卡住,"Write lock acquired" 不会出现——因为写锁被阻塞,而阻塞源正是那个尚未释放首层读锁的 goroutine。
⚠️ 关键注意事项:
- RWMutex 不支持递归读锁,如同 Mutex 不支持递归写锁一样。这不是 bug,而是明确的设计约束;
- Go 的 mutex 类型均无重入(reentrant)语义,任何重复 Lock() 或 RLock() 在持有锁状态下调用,都属未定义行为;
- 静态检查工具(如 go vet)不会捕获此类逻辑错误,需依赖代码审查或运行时检测(如启用 -race 可辅助发现竞争,但无法直接报递归锁);
- 若业务逻辑确需“多次进入读保护区域”,应重构为:只在最外层 RLock() / RUnlock(),内部函数通过参数传递已验证的只读上下文,避免重复加锁。
✅ 安全实践建议:
- 使用 defer mu.RUnlock() 仅配对一次 RLock(),确保作用域清晰;
- 对嵌套调用场景,可将读锁生命周期提升至调用栈顶层,或改用 sync.Once + 缓存等无锁策略;
- 在高并发读写混合场景下,评估是否真需 RWMutex:若写操作频繁,其写优先机制反而降低吞吐,此时普通 Mutex 或分片锁(sharded lock)可能更优。
总之,理解 RWMutex 的写优先调度模型与非递归本质,是编写健壮并发 Go 程序的关键一课。切记:一次 RLock,一次 RUnlock;绝不嵌套、不重入、不假设可重试。










