刚学 golang,学写了个 tcp server 代码, 为什么函数已经返回了,里面的一段 time.afterfunc 代码还能执行,帮我看看到底是什么原因

2019-01-17 04:13:45 +08:00
 zzlettle

最近开始学习 go,照着教程写了一个简单的 tcp server 的代码,实现了 5 秒客户端还没有任何的数据发送,就自动关闭当前连接。这个地方我用的是 time.afterfunc 来实现的 运行起来也达到了预期,但不知道为什么,明明当前连接已经关闭,处理连接的函数已经返回,但是里面的这个 time.afterfunc 包裹的函数继续执行,真是见鬼了。下面是代码

package main
import (
	"fmt"
	"net"
	"time"
)

func main() {
	l, err := net.Listen("tcp", ":8888")
	if err != nil {
		fmt.Println("listen 报错:", err)
		return
	}
	defer l.Close()
	fmt.Println("listen ok")
	var i int
	for {

		if conn, err := l.Accept(); err != nil {
			fmt.Println("accept 报错:", err)
			break
		} else {

			i++
			fmt.Printf("%d: 接收到新的连接\n", i)
			go fmt.Println(handleconntimeout(conn, 5))
		}

	}
}

func handleconntimeout(conn net.Conn, timeout int) int {
	b := make([]byte, 1024)
	var closetimer *time.Timer
	f := realfunc(conn)
	closetimer = time.AfterFunc(time.Duration(timeout)*time.Second, f)
	defer conn.Close()
	for n, err := conn.Read(b); err == nil; n, err = conn.Read(b) {

		
		closetimer.Reset(time.Duration(timeout) * time.Second)
		fmt.Printf("收到来自 %s 的一共 %d 数量字节\n", conn.RemoteAddr(), n)
		fmt.Println(string(b[:n]))

	}
	fmt.Println("连接报错")
	
	return 10000

}
func closeconn(conn net.Conn) {
	fmt.Println("时间到,关闭连接")
	conn.Close()

}
func realfunc(conn net.Conn) func() {
	return func() {
		closeconn(conn)
	}

}

我启动服务端,用客户端连接没有问题,等待 5 秒钟,服务端这边一切如预期的一样, 第一行出现:时间到,关闭连接 第二行出现:连接报错 第三行打印出 handleconntimeout 这个函数的返回值 1000

诡异的是,我这边如果主动关闭客户端,服务端出现的是 第一行打印出:连接报错 第二行打印出:handleconntimeout 的返回值 1000 我本来以为后面就不应该再继续出现任何东西了 结果过了 5 秒第三行打印出 时间到,连接关闭

奇怪了,这个明明应该在 handleconntimeout 里面存在的动作,

time.AfterFunc(time.Duration(timeout)*time.Second, f) 这个延后执行的函数,不是应该随着 handleconntimeout 的返回,不应该继续执行的啊 怎么后来还在继续执行 由于是初学 golang,高手懂的给指点指点。 谢谢

2464 次点击
所在节点    问与答
7 条回复
yuikns
2019-01-17 05:07:21 +08:00
return 10000 前加上

closetimer.Stop()
yuikns
2019-01-17 05:11:23 +08:00
另外,若你只是想要个 timeout,其实还可以这么写:


func handleconntimeout(conn net.Conn, timeout int) int {
b := make([]byte, 1024)
defer conn.Close()
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
for n, err := conn.Read(b); err == nil; n, err = conn.Read(b) {
fmt.Printf("收到来自 %s 的一共 %d 数量字节\n", conn.RemoteAddr(), n)
fmt.Println(string(b[:n]))
conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
}
return 10000
}
zzlettle
2019-01-17 06:22:21 +08:00
@yuikns 是的,我就是最后加上 closetimer.Stop()来避免。但这个还么做,还是没有让我搞清楚,为什么外面的函数已经返回了,里面的还能继续运行。
yuikns
2019-01-17 06:31:35 +08:00
@zzlettle 因为没有回收啊。

帮你找个源码:

------

func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),
f: goFunc,
arg: f,
},
}
startTimer(&t.r)
return t
}

func goFunc(arg interface{}, seq uintptr) {
go arg.(func())()
}

-----

closetimer := time.AfterFunc(time.Duration(timeout)*time.Second, f)

这个就相当于开一个 goroutine, 等到 timer 到了然后执行一下。这个要是回去就把 goroutine 里面的东西全给撤了,那 go 可以废了...
yuikns
2019-01-17 06:42:57 +08:00
哦,刚才的回复结论没啥问题,说明有问题。

startTimer 的实现参考这个链接:

https://golang.org/src/runtime/time.go#L110

读源码可知它就是在全局 goroutine 里面加上了一个 timer。 上述那个似乎是在暗示当场 go func() 。开个自定义的。但其实不是。它是先检查已有的 timer。若有多个,会共享一个 routine。然后到它的时候,回调执行 go arg(xxx). 这样可以不阻塞别的 timer。
zzlettle
2019-01-17 17:42:49 +08:00
@yuikns 原因应该跟 GO 里面的底层 goroutin 调度有关,应该是官方的有些问题,应该要改进。否则函数返回,里面还继续运行。反正我就是拿来写自己的应用,怎么能搞定怎么来。看源代码绕来绕去的,不到万不得已,真不想看。
yuikns
2019-01-18 00:14:22 +08:00
@zzlettle 你的意思是

func Foo() {
go func() {
time.Sleep(1 * time.Second)
fmt.Println("bar")
}()
return
}

这里面的 bar 应该永远不输出?

"不到万不得已,真不想看。", "应该是官方的有些问题"。这是 goroutine 一致的逻辑。此处没有特例。用 go 委屈你了,换个语言吧

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/527768

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX