登录
首页 >  Golang >  Go教程

优雅停止两个Goroutine的实用方法

时间:2026-02-01 18:36:39 155浏览 收藏

珍惜时间,勤奋学习!今天给大家带来《优雅终止两个Goroutine的技巧分享》,正文内容主要涉及到等等,如果你正在学习Golang,或者是对Golang有疑问,欢迎大家关注我!后面我会持续更新相关内容的,希望都能帮到正在学习的大家!

如何优雅地同步终止两个 Goroutine

本文介绍在 Go 中通过共享退出通道(quit channel)协调多个 Goroutine 的生命周期,确保任一 Goroutine 异常或正常退出时,其他 Goroutine 能立即响应并安全退出,避免资源泄漏和 goroutine 泄露。

在构建 WebSocket 服务(如基于 gorilla/websocket)时,常见模式是为每个连接启动两个长期运行的 Goroutine:一个负责从客户端读取消息(readFromSocket),另一个负责向客户端发送消息(writeToSocket)。理想情况下,二者应“同生共死”——任一 Goroutine 因连接断开、错误或主动关闭而退出时,另一个也应立即停止,而非继续阻塞在通道读取或等待中。

原代码的问题在于:writeToSocket 依赖 p.writeChan 的关闭来退出循环,但 close(p.writeChan) 发生在 Cleanup() 中,而 Cleanup() 又依赖 p.closeEventChan 的信号——该信号仅由 readFromSocket 单方面触发。这导致典型的竞态:若 readFromSocket 先退出并触发 Cleanup(),writeToSocket 才会收到通道关闭信号;但若 writeToSocket 因写失败先退出,则 readFromSocket 仍无限循环,造成 Goroutine 泄漏。

✅ 正确解法是引入统一的退出信号源——一个只读的 quit <-chan struct{}。所有工作 Goroutine 均监听该通道,并在接收到关闭信号(即通道被 close())时立即退出。主协程(EventLoop)在首个子 Goroutine 退出后,主动关闭 quit 通道,广播终止指令。

以下是重构后的核心实现:

func (p *Player) EventLoop() {
    l4g.Info("Starting player %s event loop", p)
    quit := make(chan struct{}) // 共享退出通道
    go p.readFromSocket(quit)
    go p.writeToSocket(quit)

    // 等待第一个 Goroutine 通知退出
    <-p.closeEventChan

    // 广播退出信号给所有协作 Goroutine
    close(quit)

    // 等待剩余 Goroutine 完成清理(此处共 2 个,已收 1 个,还需收 1 个)
    <-p.closeEventChan

    p.cleanup()
}

func (p *Player) writeToSocket(quit <-chan struct{}) {
    defer func() { p.closeEventChan <- true }() // 统一通知主协程
    for {
        select {
        case <-quit:
            return // 退出信号优先级最高
        case m, ok := <-p.writeChan:
            if !ok {
                return // writeChan 已关闭
            }
            if p.conn == nil || reflect.DeepEqual(network.Packet{}, m) {
                return
            }
            if err := p.conn.WriteJSON(m); err != nil {
                return
            }
        }
    }
}

func (p *Player) readFromSocket(quit <-chan struct{}) {
    defer func() { p.closeEventChan <- true }()
    for {
        select {
        case <-quit:
            return
        default:
            if p.conn == nil {
                return
            }
            var m network.Packet
            if err := p.conn.ReadJSON(&m); err != nil {
                return
            }
            // 处理消息逻辑(如转发到 writeChan 等)
        }
    }
}

⚠️ 关键注意事项:

  • quit 通道使用 struct{} 类型,零内存开销,语义清晰(仅作信号传递);
  • select + <-quit 是 Goroutine 退出的黄金组合,比轮询 if p.conn == nil 更高效、更及时;
  • defer 确保无论何种路径退出,都能向 p.closeEventChan 发送完成信号;
  • 主协程需按预期数量接收 closeEventChan 信号(本例为 2 次),避免因漏收导致阻塞;
  • p.writeChan 和 p.closeEventChan 本身不应在 EventLoop 中提前关闭,而应由 Cleanup() 在所有子 Goroutine 退出后统一关闭(或由 defer 隐式处理)。

这种模式可轻松扩展至 N 个协作 Goroutine(如增加 ping/pong 心跳协程),只需共享同一 quit 通道并计数等待即可,是 Go 并发编程中管理协同任务生命周期的标准实践。

终于介绍完啦!小伙伴们,这篇关于《优雅停止两个Goroutine的实用方法》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布Golang相关知识,快来关注吧!

前往漫画官网入口并下载 ➜
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>