showliuzp 2026-05-05 21:48 采纳率: 85%
浏览 3
已采纳

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.Lock/sum.Mu.Runlock是没有必要的,因为在for循环开始已经加锁了
  • 写回答

2条回答 默认 最新

  • 檀越@新空间 2026-05-05 21:49
    关注

    晚上好🌙🌙🌙
    本答案参考通义千问

    你提出的问题非常关键,确实需要仔细分析 Go 中并发安全 的逻辑。我们来逐步分析你的 HeartBeat 函数,并判断是否真的如你所说,sm.Mu.Lock()sm.Mu.Unlock() 是不必要的。


    ✅ 问题核心分析

    在你提供的代码中:

    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 循环开始时已经加了读锁(RLock),那么在循环内部对 Sessions 的访问是安全的,因此删除操作不需要再加写锁(Lock)。


    🔍 实际情况分析

    ❌ 错误点:不能在 RLock 下直接修改 map

    虽然你在 for 循环中使用了 RLock 来读取 Sessions,但你在循环中调用了 delete 操作,这会修改 map 的内容。在 Go 中,对 map 的写操作必须在写锁下进行,否则会导致数据竞争(race condition)。

    即使你只读取了 map 中的值,一旦你对 map 进行写入操作(如 delete,就必须加写锁


    ✅ 正确做法:在写操作前加写锁

    你提出的观点部分正确,但有一个重要前提:你只能在读锁下读取 map,不能在读锁下写入 map。所以:

    • 读取操作:可以使用 RLock
    • 写入操作:必须使用 Lock

    因此,在 if 条件判断后执行 delete 操作时,必须加写锁


    🧠 为什么不能在 RLock 下直接修改 map?

    Go 的 sync.RWMutex 设计是:读锁不会阻塞其他读锁,但会阻塞写锁。也就是说,如果你在一个协程中持有读锁,另一个协程尝试写入时会被阻塞。

    然而,map 的写操作本身不是原子的,它涉及多个步骤(例如:查找 key、删除 key 等),因此必须确保整个操作在写锁保护下完成


    ✅ 建议的改进方案

    下面是优化后的 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.RUnlock() // 释放读锁
            time.Sleep(10 * time.Second)
        }
    }
    

    📝 总结与重点说明

    以下是建议的处理方式:

    1. 在读取 map 时使用 RLock,这是正确的做法。
    2. 在写入 map(如 delete)时必须使用 Lock,这是 Go 并发安全的核心要求。
    3. 不能在 RLock 下直接修改 map,否则可能导致数据竞争或不可预料的行为。

    ✅ 修正后的完整代码示例(推荐)

    package main
    
    import (
        "fmt"
        "net"
        "sync"
        "time"
    )
    
    type Session struct {
        Conn   net.Conn
        Times  time.Time
    }
    
    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 {
                    sm.Mu.RUnlock()
                    sm.Mu.Lock()
                    delete(sm.Sessions, id)
                    sm.Mu.Unlock()
                }
            }
            sm.Mu.RUnlock()
            time.Sleep(10 * time.Second)
        }
    }
    

    🚨 注意事项

    • 如果你在多个地方对 Sessions 进行读写操作,要确保所有操作都遵循上述锁规则。
    • 如果性能敏感,可以考虑使用 sync.Map 替代 map,它本身就是并发安全的。

    如果你有更多关于并发控制、goroutine 管理的问题,欢迎继续提问!

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

问题事件

  • 已采纳回答 今天
  • 创建了问题 5月5日