type Session struct {
Conn net.Conn // 客户端连接
Times time.Time // 最近一次接收到消息的时间
// 可以添加更多的字段,比如连接的ID,用户信息等
}
type SessionM struct {
Sessions map[string]*Session // 存储所有连接
Mu sync.RWMutex // 用于并发安全
}
//心跳处理
func (sm *SessionM) HeartBeat() {
for {
sm.Mu.RLock() // 加锁,确保读取连接列表时是线程安全的
for id, session := range sm.Sessions {
if time.Now().Sub(session.Times) > 30*time.Second { // 如果超过30秒未收到心跳包
sm.Mu.RUnlock() // 释放锁
fmt.Printf("Connection %s timed out, removing...\n", id)
sm.Mu.Lock() // 加锁,开始删除连接
delete(sm.Sessions, id) // 删除超时连接
sm.Mu.Unlock() // 解锁
}
}
sm.Mu.RUnlock() // 释放读锁
time.Sleep(10 * time.Second) // 每10秒进行一次检测
}
}
假如HeartBeat函数里 if time.Now().Sub(session.Times) > 30*time.Second 的条件满足, sm.Mu.RUnlock() //释放读锁
那么time.Sleep(10 * time.Second)上一行的 sm.Mu.RUnlock() 岂不报错:fatal error : sync: RUnlock of unlocked RWMutex?
golang map保存tcp 连接加锁释放处理
- 写回答
- 好问题 0 提建议
- 关注问题
- 邀请回答
-
2条回答 默认 最新
檀越@新空间 2026-05-05 22:39关注晚上好🌙🌙🌙
本答案参考通义千问你提出的问题非常关键,确实存在一个 严重的并发安全问题,会导致
sync: RUnlock of unlocked RWMutex的 panic。
🚨 问题分析
在你的
HeartBeat函数中,代码如下:func (sm *SessionM) HeartBeat() { for { sm.Mu.RLock() // 加读锁 for id, session := range sm.Sessions { if time.Now().Sub(session.Times) > 30*time.Second { sm.Mu.RUnlock() // 释放读锁 fmt.Printf("Connection %s timed out, removing...\n", id) sm.Mu.Lock() // 加写锁 delete(sm.Sessions, id) sm.Mu.Unlock() // 解锁 } } sm.Mu.RUnlock() // 释放读锁 time.Sleep(10 * time.Second) } }问题点在于:
- 在
for循环内部,当检测到超时连接时,先调用了RUnlock(),这会提前释放了读锁。 - 然后在循环外部又尝试调用
RUnlock(),此时 已经没有锁了,就会导致 panic:sync: RUnlock of unlocked RWMutex。
✅ 正确的处理方式
要避免这个错误,必须确保 每次加锁后都正确地解锁一次。因此,不能在循环中提前释放读锁。
🔧 解决方案
以下是修改后的
HeartBeat函数,确保加锁和解锁是成对出现的,并且只在必要时加写锁。✅ 修改后的代码如下:
func (sm *SessionM) HeartBeat() { for { sm.Mu.RLock() // 加读锁 // 遍历所有连接 for id, session := range sm.Sessions { if time.Now().Sub(session.Times) > 30*time.Second { sm.Mu.RUnlock() // 先释放读锁,再加写锁 sm.Mu.Lock() delete(sm.Sessions, id) sm.Mu.Unlock() sm.Mu.RLock() // 再次获取读锁,继续遍历 } } sm.Mu.RUnlock() // 最终释放读锁 time.Sleep(10 * time.Second) } }
📌 重点说明(加粗部分)
- ⚠️ 不可以在循环中提前释放读锁,否则会导致最终无法正确解锁,引发 panic。
- 正确的做法是:
- 在循环外加读锁;
- 在需要修改数据时,先释放读锁,再加写锁;
- 修改完成后,加回读锁(如果还需要继续遍历);
- 最后统一在循环外释放读锁。
🧠 补充建议
如果你希望更高效地管理连接,可以考虑以下优化:
- 使用
sync.Map替代map[string]*Session,因为sync.Map是线程安全的,不需要手动加锁。 - 将超时检查与删除操作分离,比如使用定时器或 goroutine 来异步处理。
- 避免在
HeartBeat中频繁加锁/解锁,影响性能。
✅ 总结
| 问题 | 原因 | 解决方案 | |------|------|----------| | panic: sync: RUnlock of unlocked RWMutex | 在循环中提前释放了读锁,导致最终无法正确解锁 | 在循环中不要提前释放读锁,统一在循环外释放 |
如需进一步优化或实现其他功能(如连接管理、心跳包处理等),也可以继续提问。
解决 无用评论 打赏 举报- 在