简体   繁体   English

如何使用 for 循环在多个 goroutines 之间进行通信,并在其中一个阻塞函数调用

[英]How to communicate between multiple goroutines with for loops with blocking function calls inside one of them

I'm writing a Go app which accepts a websocket connection, then starts:我正在编写一个接受 websocket 连接的 Go 应用程序,然后启动:

  1. listen goroutine which listens on the connection for client messages and sends response for the client based on the received message via channel to updateClient . listen goroutine 监听客户端消息的连接,并根据收到的消息通过通道向updateClient发送客户端响应。
  2. updateClient goroutine which writes to the connection. updateClient goroutine 写入连接。
  3. processExternalData goroutine which receives data from message queue, sends the data to updateClient via a channel so that updateClient can update the client with the data. processExternalData goroutine 从消息队列接收数据,通过通道将数据发送到updateClient ,以便updateClient可以使用数据更新客户端。

I'm using gorilla library for websocket connections, and itsread call is blocking.我正在使用gorilla库进行 websocket 连接,它的读取调用被阻塞。 In addition, both its write and read methods don't support concurrent calls, which is the main reason I have the updateClient goroutine which is the single routine which calls write method.此外,它的写入和读取方法 都不支持并发调用,这也是我拥有updateClient goroutine 的主要原因,它是调用 write 方法的单个例程。

The problem arises when I need to close the connection which can happen at least in 2 cases:当我需要关闭至少在两种情况下可能发生的连接时,就会出现问题:

  1. The client closed the connection or error occurred during read.客户端关闭了连接或读取过程中发生错误。
  2. processExternalData finished, there's no more data to update the client and the connection should be closed. processExternalData完成,没有更多的数据来更新客户端,连接应该关闭。

So updateClient needs to somehow notify listen to quit and vice versa listen needs to somehow notify updateClient to quit.所以updateClient需要以某种方式通知listen退出,反之亦然listen需要以某种方式通知updateClient退出。 updateClient has a quit channel inside select but listen can't have select because it already has a for loop with blocking read call inside. updateClientselect有一个退出通道,但listen不能有select因为它已经有一个for循环,内部阻止了 read 调用。

So what I did is I added isJobFinished field on the connection type which is a condition for for loop to work:所以我所做的是在连接类型上添加了isJobFinished字段,这是for循环工作的条件:

type WsConnection struct {
    connection    *websocket.Conn
    writeChan     chan messageWithCb
    quitChan      chan bool
    isJobFinished bool
    userID        string
}

func processExternalData() {
    // receive data from message queue
    // send it to client via writeChan
}

func (conn *WsConnection) listen() {
    defer func() {
        conn.connection.Close()
        conn.quitChan <- true
    }()

    // keep the loop for communication with client
    for !conn.isJobFinished {
        _, message, err := conn.connection.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break

        }
        // convert message to type messageWithCb
        switch clientMessage.MessageType {
        case userNotFound:
            conn.writeChan <- messageWithCb{
                message: map[string]interface{}{
                    "type":    user,
                    "payload": false,
                },
            }
        default:
            log.Printf("Unknown message type received: %v", clientMessage)
        }
    }
    log.Println("end of listen")
}

func updateClient(w http.ResponseWriter, req *http.Request) {
    upgrader.CheckOrigin = func(req *http.Request) bool {
        return true
    }
    connection, err := upgrader.Upgrade(w, req, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    wsConn := &WsConnection{
        connection: connection,
        writeChan:  make(chan messageWithCb),
        quitChan:   make(chan bool),
    }
    go wsConn.listen()
    for {
        select {
        case msg := <-wsConn.writeChan:
            err := connection.WriteJSON(msg.message)
            if err != nil {
                log.Println("connection.WriteJSON error: ", err)
            }
            if wsConn.isJobFinished {
                if msg.callback != nil {
                    msg.callback() // sends on `wsConn.quitChan` from a goroutine
                }
            }
        case <-wsConn.quitChan:
            // clean up
            wsConn.connection.Close()
            close(wsConn.writeChan)
            return
        }
    }
}

I'm wondering if a better pattern exists in Go for such cases.对于这种情况,我想知道 Go 中是否存在更好的模式。 Specifically, I'd like to be able to have a quit channel inside listen as well so updateClient can notify it to quit instead of maintaining isJobFinished field.具体来说,我希望能够在listen内有一个退出频道,以便updateClient可以通知它退出而不是维护isJobFinished字段。 Also in this case there's no danger of not protecting isJobFinished field because only one method writes to it but if the logic gets more complicated then having to protect the field inside the for loop in listen will probably negatively impact the performance.同样在这种情况下,不保护isJobFinished字段没有危险,因为只有一种方法写入它,但如果逻辑变得更复杂,那么必须保护listen for循环内的字段可能会对性能产生负面影响。

Also I can't close the quiteChan because both listen and updateClient use it and there's no way to know for them when it's closed by another one.此外,我无法关闭quiteChan因为listenupdateClient使用它,并且无法知道它们何时被另一个关闭。

Close the connection to break the listen goroutine out of the blocking read call.关闭连接以中断listen goroutine 的阻塞读取调用。

In updateClient , add a defer statement to close the connection and clean up other resources.updateClient ,添加 defer 语句来关闭连接并清理其他资源。 Return from the function on any error or a notification from the quit channel:从函数返回任何错误或来自退出通道的通知:

updateClient(w http.ResponseWriter, req *http.Request) {
    upgrader.CheckOrigin = func(req *http.Request) bool {
        return true
    }
    connection, err := upgrader.Upgrade(w, req, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer connection.Close() // <--- Add this line
    wsConn := &WsConnection{
        connection: connection,
        writeChan:  make(chan messageWithCb),
        quitChan:   make(chan bool),
    }
    defer close(writeChan) // <-- cleanup moved out of loop below.
    go wsConn.listen()
    for {
        select {
        case msg := <-wsConn.writeChan:
            err := connection.WriteJSON(msg.message)
            if err != nil {
                log.Println("connection.WriteJSON error: ", err)
                return
            }
        case <-wsConn.quitChan:
            return
        }
    }
}

In the listen function, loop until error reading the connection.listen函数中,循环直到读取连接出错。 Read on the connection returns immediately with an error when updateClient closes the connection.updateClient关闭连接时,连接上的读取会立即返回并显示错误。

To prevent listen from blocking forever in the case where updateClient returns first, close the quit channel instead of sending a value.为了防止在updateClient首先返回的情况下listen永远阻塞,请关闭退出通道而不是发送值。

func (conn *WsConnection) listen() {
    defer func() {
        conn.connection.Close()
        close(conn.quitChan) // <-- close instead of sending value
    }()

    // keep the loop for communication with client
    for  {
        _, message, err := conn.connection.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break

        }
        // convert message to type messageWithCb
        switch clientMessage.MessageType {
        case userNotFound:
            conn.writeChan <- messageWithCb{
                message: map[string]interface{}{
                    "type":    user,
                    "payload": false,
                },
            }
        default:
            log.Printf("Unknown message type received: %v", clientMessage)
        }
    }
    log.Println("end of listen")
}

The field isJobFinished is not needed.不需要字段isJobFinished

One problem with the code in the question and in this answer is that close of writeChan is not coordinated with sends to the channel.问题和此答案中的代码的一个问题是writeChan关闭与发送到通道不协调。 I cannot comment on a solution to this problem without seeing the processExternalData function.如果没有看到processExternalData函数,我就无法评论这个问题的解决方案。

It may make sense to use a mutex instead of a goroutine to limit write concurrency.使用互斥体而不是 goroutine 来限制写入并发性可能是有意义的。 Again, the code in the processExternalData function is required to comment further on this topic.同样, processExternalData函数中的代码需要进一步评论这个主题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM