【问题标题】:What is the right way to safely finish goroutines in my code?在我的代码中安全完成 goroutine 的正确方法是什么?
【发布时间】:2018-12-10 09:22:12
【问题描述】:

我正在写一个简单的 tcp 服务器,goroutine 模型非常简单:

一个goroutine负责接受新的连接;对于每个新连接,都会启动三个 goroutine:

  1. 供阅读
  2. 一个用于处理和处理应用程序逻辑
  3. 一个写

目前一台服务器服务的用户不超过 1000 个,所以我不尝试限制 goroutine 数量。

for {
    conn, err := listener.Accept()
    // ....
    connHandler := connHandler{
        conn:      conn,
        done:      make(chan struct{}),
        readChan:  make(chan string, 100),
        writeChan: make(chan string, 100),
    }
    // ....
    go connHandler.readAll()    
    go connHandler.processAll() 
    go connHandler.writeAll()   
}

我使用done通道通知所有三个通道完成,当用户注销或发生永久性网络错误时,done通道将被关闭(使用sync.Once确保关闭只发生一次):

func (connHandler *connHandler) Close() {
    connHandler.doOnce.Do(func() {
        connHandler.isClosed = true
        close(connHandler.done)
    })
}

下面是writeAll()方法的代码:

func (connHandler *connHandler) writeAll() {
    writer := bufio.NewWriter(connHandler.conn)

    for {
        select {
        case <-connHandler.done:
            connHandler.conn.Close()
            return
        case msg := <-connHandler.writeChan:
            connHandler.writeOne(msg, writer)
        }
    }
}

有一个Send 方法可以通过向写入通道发送字符串来向用户发送消息:

func (connHandler *connHandler) Send(msg string) {
    case connHandler.writeChan <- msg:
}

Send 方法将主要在processAll() goroutine 中调用,但也会在许多其他 goroutine 中调用,因为不同的用户需要相互通信。

现在的问题是:如果用户 A 注销或网络失败,用户 B 向用户 A 发送消息,用户 B 的 goroutine 可能会被永久阻塞,因为没有人会收到来自频道的消息。

我的解决方案:

我的第一个想法是使用布尔值来确保 connHanler 在发送给它时没有关闭:

func (connHandler *connHandler) Send(msg string) {
    if !connHandler.isClosed {
        connHandler.writeChan <- msg
    }
}

但是我觉得connHandler.writeChan &lt;- msgclose(done)还是可以同时发生的,阻塞的可能性还是存在的。所以我必须添加一个超时:

func (connHandler *connHandler) Send(msg string) {
    if !connHandler.isClosed {
        timer := time.NewTimer(10 * time.Second)
        defer timer.Stop()
        select {
        case connHandler.writeChan <- msg:
        case <-timer.C:
            log.Warning(connHandler.Addr() + " send msg timeout:" + msg)
        }
    }

}

现在我觉得代码很安全,但也很丑,每次发送消息时都启动一个计时器,感觉像是不必要的开销。

然后我看了这篇文章:https://go101.org/article/channel-closing.html,我的问题看起来像文章中的第二个例子:

一个接收者,N个发送者,接收者说“请停止发送更多” 通过关闭额外的信号通道

但我认为这种解决方案并不能消除我这种情况下阻塞的可能性。

也许最简单的解决方案是关闭写入通道并让Send 方法恐慌,然后使用recover 处理恐慌?但这看起来也很丑陋。

那么有没有一种简单直接的方法来完成我想做的事情?

(本人英文不好,如有歧义,请指出,谢谢。)

【问题讨论】:

  • 在您的Send() 中,也许您可​​以在接收用户B 的connHandler.doneselect

标签: go channel goroutine


【解决方案1】:

你的例子看起来不错,我认为你已经得到了你需要的 90%。

我认为您看到的问题在于发送,而实际上您可能已经“完成”了。

您可以使用“完成”频道通知所有您已完成的 goroutine。您将始终能够从关闭的通道中读取一个值(它将是零值)。这意味着您可以更新您的 Send(msg) 方法以考虑完成通道。

func (connHandler *connHandler) Send(msg string) {
    select {
    case connHandler.writeChan <- msg:
    case <- connHandler.done:
        log.Debug("connHandler is done, exiting Send without sending.")
    case <-time.After(10 * time.Second):
        log.Warning(connHandler.Addr() + " send msg timeout:" + msg)
    }
}

现在此选择中会发生以下情况之一:

  1. 消息发送到writeChan
  2. close(done) 已在别处调用,done chan 已关闭。您将能够从完成中读取,打破选择。
  3. time.After(...) 将达到 10 秒,您将能够使发送超时。

【讨论】:

  • 我认为在这里使用select确实可以解决我的问题,很简单但我没有看到它!非常感谢。
猜你喜欢
  • 2014-11-15
  • 2013-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-11
  • 2017-12-09
  • 2012-11-30
  • 2021-11-17
相关资源
最近更新 更多