在我的代码中安全完成goroutine的正确方法是什么?

时间:2018-07-02 07:41:15

标签: go channel goroutine

我正在编写一个简单的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通道将被关闭(使用同步。请确保仅关闭关闭一次):

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 <- 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处理紧急情况?但这也看起来很丑。

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

(我的英语不好,所以如果有歧义,请指出,谢谢。)

1 个答案:

答案 0 :(得分:4)

您的示例看起来不错,我认为您已经满足了90%的需求。

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

您可以使用“完成”频道将已完成的go例程通知所有。您将始终能够从封闭通道中读取一个值(该值为零)。这意味着您可以更新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)在其他地方已被调用,完成的操作已关闭。您将能够从完成中读取内容,从而打破选择。
  3. time.After(...)将达到10秒,您将可以使发送超时。