showliuzp 2026-05-05 22:38 采纳率: 84.5%
浏览 2

golang map保存tcp 连接加锁释放处理


 
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?
  • 写回答

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。
    • 正确的做法是:
      1. 在循环外加读锁;
      2. 在需要修改数据时,先释放读锁,再加写锁
      3. 修改完成后,加回读锁(如果还需要继续遍历);
      4. 最后统一在循环外释放读锁。

    🧠 补充建议

    如果你希望更高效地管理连接,可以考虑以下优化:

    1. 使用 sync.Map 替代 map[string]*Session,因为 sync.Map 是线程安全的,不需要手动加锁。
    2. 将超时检查与删除操作分离,比如使用定时器或 goroutine 来异步处理。
    3. 避免在 HeartBeat 中频繁加锁/解锁,影响性能。

    ✅ 总结

    | 问题 | 原因 | 解决方案 | |------|------|----------| | panic: sync: RUnlock of unlocked RWMutex | 在循环中提前释放了读锁,导致最终无法正确解锁 | 在循环中不要提前释放读锁,统一在循环外释放 |


    如需进一步优化或实现其他功能(如连接管理、心跳包处理等),也可以继续提问。

    评论

报告相同问题?

问题事件

  • 创建了问题 5月5日