[英]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 应用程序,然后启动:
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
发送客户端响应。updateClient
goroutine which writes to the connection. updateClient
goroutine 写入连接。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:当我需要关闭至少在两种情况下可能发生的连接时,就会出现问题:
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. updateClient
在select
有一个退出通道,但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
因为listen
和updateClient
使用它,并且无法知道它们何时被另一个关闭。
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.