Go:使用Go通道和select时的死锁问题

huangapple go评论111阅读模式
英文:

Go : Deadlock issue with go channel and select

问题

我已经在golang中实现了一个演示的TCP聊天服务器,它工作得很好,但是每当一个用户断开连接并且我尝试向广播通道写入一条消息以通知其他用户有用户断开连接时,它会阻塞,并且不会进一步处理来自其他客户端的任何新消息,因为它是一个非缓冲通道。

我已经对代码进行了注释并解释了它,你可以查看一下,我不知道为什么代码会阻塞,我已经写了消息:

  1. 我要写入一个通道
  2. 我已经写入了通道
  3. 我已经从通道中读取了数据

而且消息的顺序完全正确,但是我的消息通道仍然阻塞。

附注:如果我使用缓冲通道,代码就不会阻塞,但是我想知道我的代码在哪里被卡住了。
我还尝试使用-race标志运行我的代码,但没有帮助。

package main

import (
	"fmt"
	"io"
	"net"
	"sync"
)

func main() {
	msg := make(chan string)          //广播通道(将其更改为缓冲通道问题就会消失)
	allConn := make(map[net.Conn]int) //用于广播消息的传入连接的集合
	disConn := make(chan net.Conn)    //客户端断开连接通道
	newConn := make(chan net.Conn)    //新客户端连接通道
	mutext := new(sync.RWMutex)       //用于为传入连接分配唯一ID的互斥锁
	i := 0
	listener, err := net.Listen("tcp", "127.0.0.1:8081")
	checkErr(err)
	fmt.Println("Tcp server started at 127.0.0.1:8081")
	//接受传入连接并将其存储在全局连接存储allConn中
	go func() {
		for {
			conn, err := listener.Accept()
			checkErr(err)
			mutext.Lock()
			allConn[conn] = i
			i++
			mutext.Unlock()
			newConn <- conn
		}
	}()
	for {
		select {
		//等待新的客户端消息到达并广播消息
		case umsg := <-msg:
			fmt.Println("Broadcast Channel: Already Read")
			bmsg := []byte(umsg)
			for conn1, _ := range allConn {
				_, err := conn1.Write(bmsg)
				checkErr(err)
			}

		//处理客户端断开连接[disConn]
		case conn := <-disConn:
			mutext.RLock()
			fmt.Println("user disconneting", allConn[conn])
			mutext.RUnlock()
			delete(allConn, conn)
			fmt.Println("Disconnect: About to Write")
			//即使通道为空,此调用也会导致死锁,缓冲通道可以解决此问题
			//需要知道为什么
			msg <- fmt.Sprintf("Disconneting", allConn[conn])
			fmt.Println("Disconnect: Already Written")

		//读取客户端传入消息并将其放入广播通道,在断开连接时放入disConn通道
		case conn := <-newConn:
			go func(conn net.Conn) {
				for {
					buf := make([]byte, 64)
					n, err := conn.Read(buf)
					if err != nil {
						if err == io.EOF {
							disConn <- conn
							break
						}
					}
					fmt.Println("Client: About to Write")
					msg <- string(buf[0:n])
					fmt.Println("Client: Already Written")
				}
			}(conn)
			mutext.RLock()
			fmt.Println("User Connected", allConn[conn])
			mutext.RUnlock()
		}
	}
}
func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}
英文:

I've implemented a demo tcp chat server in golang, it works fine, but every time a user disconnects and I try to write a message to the broadcast channel to let other users know a user has disconnected it blocks, and would not further process any new messages from other client because its a nonbuffered channel

I've commented by code and explained it you can go through it, I don't know why the code blocks, I've written msgs

  1. I'm about to write to a channel
  2. I've written to the channel
  3. I've read from the channel

and messages are in perfect order still my msg channel blocks.

Ps: If I'm using buffered channel the code is not blocking, but I want to know where is my code getting stuck.
I also tried running my code with -race flag but no help

package main
import (
&quot;fmt&quot;
&quot;io&quot;
&quot;net&quot;
&quot;sync&quot;
)
func main() {
msg := make(chan string)          //broadcast channel (making it buffered channel the problem goes away)
allConn := make(map[net.Conn]int) //Collection of incoming connections for broadcasting the message
disConn := make(chan net.Conn)    //client disconnect channel
newConn := make(chan net.Conn)    //new client connection channel
mutext := new(sync.RWMutex)       //mux to assign unique id to incoming connections
i := 0
listener, err := net.Listen(&quot;tcp&quot;, &quot;127.0.0.1:8081&quot;)
checkErr(err)
fmt.Println(&quot;Tcp server started at 127.0.0.1:8081&quot;)
//Accept incoming connections and store them in global connection store allConn
go func() {
for {
conn, err := listener.Accept()
checkErr(err)
mutext.Lock()
allConn[conn] = i
i++
mutext.Unlock()
newConn &lt;- conn
}
}()
for {
select {
//Wait for a new client message to arrive and broadcast the message
case umsg := &lt;-msg:
fmt.Println(&quot;Broadcast Channel: Already Read&quot;)
bmsg := []byte(umsg)
for conn1, _ := range allConn {
_, err := conn1.Write(bmsg)
checkErr(err)
}
//Handle client disconnection [disConn]
case conn := &lt;-disConn:
mutext.RLock()
fmt.Println(&quot;user disconneting&quot;, allConn[conn])
mutext.RUnlock()
delete(allConn, conn)
fmt.Println(&quot;Disconnect: About to Write&quot;)
//this call results in deadlock even when channel is empty, buffered channel resolves the issue
//need to know why
msg &lt;- fmt.Sprintf(&quot;Disconneting&quot;, allConn[conn])
fmt.Println(&quot;Disconnect: Already Written&quot;)
//Read client incoming message and put it on broadcasting channel and upon disconnect put on it disConn channel
case conn := &lt;-newConn:
go func(conn net.Conn) {
for {
buf := make([]byte, 64)
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
disConn &lt;- conn
break
}
}
fmt.Println(&quot;Client: About to Write&quot;)
msg &lt;- string(buf[0:n])
fmt.Println(&quot;Client: Already Written&quot;)
}
}(conn)
mutext.RLock()
fmt.Println(&quot;User Connected&quot;, allConn[conn])
mutext.RUnlock()
}
}
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}

答案1

得分: 4

在Go语言中,一个无缓冲通道是一个"同步点"。也就是说,如果你有一个通道 c,并执行 c <- value,那么goroutine会阻塞,直到有人准备好执行 v = <- c(反之亦然,从一个阻塞的通道接收数据时,如果没有数据可接收,会一直阻塞,直到有数据可用,但这可能不太令人惊讶)。具体来说,对于一个阻塞的通道,接收操作会在发送操作完成之前完成。

由于你只有一个goroutine,它将无法循环回到从通道读取数据的步骤,因此写操作将被阻塞,直到有其他地方可以读取数据。

理论上,你可以通过类似以下的方式解决这个问题:go func() { msg <- fmt.Sprintf("Disconneting", allConn[conn]) }(),也就是创建一个短暂的goroutine来执行写操作。

英文:

In Go, an unbuffered channel is a "synchronisation point". That is, if you have a channel c, and do c &lt;- value, the goroutine blocks until someone is ready to do v = &lt;- c (and the converse holds, receiving from a blocking channel without something to receive blocks until the value is available, but this is possibly less surprising). Specifically, for a blocking channel, the receive completes before the send completes.

Since you only have a single goroutine, it will be unable to loop back to reading from the channel and the write will block until something can read.

You could, in theory, get around this by doing something like: go func() { msg &lt;- fmt.Sprintf(&quot;Disconneting&quot;, allConn[conn] }(), so essentially spawning a short-lived goroutine to do the write.

huangapple
  • 本文由 发表于 2017年1月16日 18:37:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/41674415.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定