我一个TCP服务,希望从外部关闭Listener,但这样就在listener.Close()与listener.Accept()之间产生了race。请教有什么好的解决办法?
简单的示例代码如下, 加"-race"参数运行,再ctrl+c退出运行,就会报race问题:
```
package main
import (
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
)
var listener *net.TCPListener
// tcp服务
func tcpServer() {
addr, err := net.ResolveTCPAddr("tcp4", ":6666")
if err != nil {
log.Fatalln(err)
}
listener, err = net.ListenTCP("tcp4", addr)
if err != nil {
log.Fatalln(err)
}
for {
c, err := listener.AcceptTCP()
if err != nil {
if err.(*net.OpError).Err == net.ErrClosed {
log.Println("listener closed")
return
}
log.Println(err)
continue
}
connHandler(c)
}
}
// 新连接处理
func connHandler(c *net.TCPConn) {
defer func() {
c.Close()
log.Printf("%s closed", c.RemoteAddr().String())
}()
log.Printf("New connection: %s", c.RemoteAddr().String())
buf := make([]byte, 128)
for {
n, err := c.Read(buf)
if err != nil {
return
}
log.Println(buf[:n])
}
}
// 关闭listener
func listenerClose() {
if listener != nil {
listener.Close()
}
}
func main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go tcpServer()
<-sigs
listenerClose()
time.Sleep(time.Millisecond * 200)
}
```
@xwszt</a> , 最初是用channel来通知关闭的。外部goroutine close掉这个channel来通知。但这样需要在创建listener的协程中另起一个协程来监听这个channel。多个listener就起了多个监听的协程,感觉浪费。
大致代码如下:
```
func NewSrv() {
ln, err := net.ListenTCP()
defer ln.Close()
...
go func() {
<- exitChann
ln.Close()
}
for {
c, err:= ln.Accept()
....
}
}
```
现在改成了1楼的建议:
```
func NewSrv() {
ln, err := net.ListenTCP()
defer ln.Close()
...
for {
select {
case <- exitChann: return
default:
ln.SetDeadlin()
c, err := ln.Accept()
....
}
}
}
```" name="content" class="comment-textarea" rows="8" style="width: 100%;"><a href="/user/xwszt" title="@xwszt">@xwszt</a> , 最初是用channel来通知关闭的。外部goroutine close掉这个channel来通知。但这样需要在创建listener的协程中另起一个协程来监听这个channel。多个listener就起了多个监听的协程,感觉浪费。
大致代码如下:
```
func NewSrv() {
ln, err := net.ListenTCP()
defer ln.Close()
...
go func() {
<- exitChann
ln.Close()
}
for {
c, err:= ln.Accept()
....
}
}
```
现在改成了1楼的建议:
```
func NewSrv() {
ln, err := net.ListenTCP()
defer ln.Close()
...
for {
select {
case <- exitChann: return
default:
ln.SetDeadlin()
c, err := ln.Accept()
....
}
}
}
```
问题的关键是listener非线程安全的,但又不好用加锁等方法来做同步
#11
更多评论
我的写法核心就是让`Accept()`操作设置超时时间,保证可以执行其他代码。
或者关闭代码就在你的程序里面模拟tcp连接发送特殊字符,然后服务端收到特殊字符就走退出逻辑。
```go
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
)
// tcp服务
func tcpServer(ctx context.Context) {
addr, err := net.ResolveTCPAddr("tcp4", ":6666")
if err != nil {
log.Fatalln(err)
}
listener, err := net.ListenTCP("tcp4", addr)
if err != nil {
log.Fatalln(err)
}
for {
select {
case <-ctx.Done():
err = listener.Close()
if err != nil {
log.Fatalln(err)
}
return
default:
// 控制超时时间,让代码有机会执行ctx.Done()
_ = listener.SetDeadline(time.Now().Add(time.Second))
c, err := listener.AcceptTCP()
if err != nil {
if opErr, ok := err.(*net.OpError); ok {
if opErr.Timeout() {
continue
}
if opErr.Err == net.ErrClosed {
log.Println("listener closed")
return
}
}
log.Println(err)
continue
}
connHandler(c)
}
}
}
// 新连接处理
func connHandler(c *net.TCPConn) {
defer func() {
c.Close()
log.Printf("%s closed", c.RemoteAddr().String())
}()
log.Printf("New connection: %s", c.RemoteAddr().String())
buf := make([]byte, 128)
for {
n, err := c.Read(buf)
if err != nil {
return
}
log.Println(buf[:n])
}
}
func main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
go tcpServer(ctx)
<-sigs
cancel()
time.Sleep(time.Millisecond * 200)
}
```
#1