go 如何知道tcpconn已断开_go语言死循环分析

最近看了一篇文章,如何定位 golang 进程 hang 死的 bug,里面有这样一段代码:

c366ceaae9b14f8ffdd0c85f282dbd14.png

运行,会发现打印一会儿数字后停了,我们执行

curl localhost:12345

程序卡死。关于程序挂在哪里借助dlv是很好定位的:

dlv debug hang.go

进去之后运行程序,打印停止进入卡死状态,我们执行ctrl C,dlv会显示断开的地方:

303cc490cb35feabe7704e0ab9f6c674.png

但是我还是不明白,不明白的地方主要是因为:

  • 我又看了两篇文章Goroutine调度实例简要分析也谈goroutine调度器,是同一位作者Tony Bai写的,写得非常好。第二篇文章解释了goroutine的调度和cpu数量的关系(不多加解释,建议大家看看),我的mac是双核四线程(这里不明白的同学自行google cpu 超线程),go版本是1.9,理论上讲可以跑4个goroutine而不用考虑死循环,一个死循环最多把一个cpu打死,上面的代码中只有3个goroutine,而且他们看上去都挂住了。
  • 上面说的理论上讲,不是我主观臆测的,我跑了1中第一篇文章中的一个例子:
eeb7fb52a5c95f5443e103edd123127a.png

上面代码有两个goroutine,一个是main goroutine,一个是deadloop goroutine,跑得时候deadloop gouroutine不会对main goroutine造成影响,打印一直在持续,作者的文章解释了原因。

  • 如何定位 golang 进程 hang 死的 bug这篇文章提到了gcwaiting,然而没有解释。

如何定位 golang 进程 hang 死的 bug有这样一段话:

因为在 for 循环中没有函数调用的话,编译器不会插入调度代码,所以这个执行 for 循环的 goroutine 没有办法被调出,而在循环期间碰到 gc,那么就会卡在 gcwaiting 阶段,并且整个进程永远 hang 死在这个循环上。并不再对外响应。

这个其实就是我们的第一段代码卡死的原因,也是我们第二段代码没有卡死的原因,就是在gc上!

我们再看一篇文章,golang的垃圾回收(GC)机制,这篇文章很短,但每句话都很重要:

  • 设置gcwaiting=1,这个在每一个G任务之前会检查一次这个状态,如是,则会将当前M 休眠;
  • 如果这个M里面正在运行一个长时间的G任务,咋办呢,难道会等待这个G任务自己切换吗?这样的话可要等10ms啊,不能等!坚决不能等!
  • 所以会主动发出抢占标记(类似于上一篇),让当前G任务中断,再运行下一个G任务的时候,就会走到第1步

那么如果这时候运行的是没有函数调用的死循环呢,gc也发出了抢占标记,但是如果死循环没有函数调用,就没有地方被标记,无法被抢占,那就只能设置gcwaiting=1,而M没有休眠,stop the world卡住了(死锁),gcwaiting一直是1,整个程序都卡住了!

这里其实已经解释了第一份代码的现象,第二份代码为什么没有hang住相信大家也能猜到了:代码里没有触发gc!我们来手动触发一下:

47a716f00652f2e3a7857aebdc5a75e1.png

会发现打印了3行之后,程序也卡死了,bingo?

我们来看看gcwaiting是不是等于1:

3224981073346fd63b152ef9fd3a6a41.png
代码诚不欺我也!本文作者:Youmai の Blog
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言的标准库中已经实现了 epoll 机制,因此可以直接使用它来编写网络程序。下面是一个简单的使用 epoll 的例子: ```go package main import ( "fmt" "net" "syscall" ) func main() { // 创建监听 socket listener, err := net.Listen("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("Error listening:", err) return } defer listener.Close() // 创建 epoll 实例 epollFd, err := syscall.EpollCreate1(0) if err != nil { fmt.Println("Error creating epoll:", err) return } // 将监听 socket 添加到 epoll 实例中 event := syscall.EpollEvent{Events: syscall.EPOLLIN, Fd: int32(listener.(*net.TCPListener).Fd())} err = syscall.EpollCtl(epollFd, syscall.EPOLL_CTL_ADD, int(listener.(*net.TCPListener).Fd()), &event) if err != nil { fmt.Println("Error adding listener to epoll:", err) return } // 循环处理 epoll 事件 events := make([]syscall.EpollEvent, 10) for { n, err := syscall.EpollWait(epollFd, events, -1) if err != nil { fmt.Println("Error waiting for epoll events:", err) return } for i := 0; i < n; i++ { if int(events[i].Fd) == int(listener.(*net.TCPListener).Fd()) { // 接受连接,并将连接 socket 添加到 epoll 实例中 conn, err := listener.Accept() if err != nil { fmt.Println("Error accepting connection:", err) continue } event := syscall.EpollEvent{Events: syscall.EPOLLIN, Fd: int32(conn.(*net.TCPConn).Fd())} err = syscall.EpollCtl(epollFd, syscall.EPOLL_CTL_ADD, int(conn.(*net.TCPConn).Fd()), &event) if err != nil { fmt.Println("Error adding connection to epoll:", err) continue } } else { // 处理连接上的数据 conn := net.Conn(connFd) buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { fmt.Println("Error reading from connection:", err) syscall.EpollCtl(epollFd, syscall.EPOLL_CTL_DEL, int(conn.(*net.TCPConn).Fd()), nil) conn.Close() continue } fmt.Println("Received:", string(buffer[:n])) } } } } ``` 在上面的例子中,首先创建了一个 TCP 监听 socket,并将它添加到 epoll 实例中。然后在循环中等待 epoll 事件,并处理连接上的数据。当有新的连接到达时,先接受连接,并将连接 socket 添加到 epoll 实例中。当连接上有数据到达时,从连接 socket 中读取数据并进行处理。 需要注意的是,在使用 epoll 时需要手动设置 socket 为非阻塞模式,否则会导致 epoll_wait 函数一直阻塞。可以使用 Go 语言的 runtime.SetNonblock 函数来设置 socket 为非阻塞模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值