当前位置: 首页 > 后端技术 > PHP

和面试官聊聊Goroutine泄露的6个方法,真刺激!

时间:2023-03-29 14:36:35 PHP

微信搜索【脑补炸鱼】关注这条炸肝炸鱼。本文GitHubgithub.com/eddycjy/blog已收录,附有我的系列文章、资料和开源Go书籍。大家好,我是炸鱼。前几天在分享一篇关于Go群问的文章时,有读者在朋友圈提到希望我能解释一下Goroutineleak。他经常在采访中被问到。今天的主角是Go语言的著名品牌标识Goroutine,一个随便就能把几十万辆快车开进车道的大杀器。for{gofunc(){}()}本文将针对Goroutine泄露的N个方法进行详细的说明和解释。你问面试官为什么会问Goroutine(协程)来泄露这么奇特的问题?可以猜想,Goroutine的使用门槛太低了,一个人就可以入手,滥用的案例也不少。例如:并发映射。Goroutine本身在Go语言的标准库、复合类型、底层源码中都有广泛的应用。例如:HTTPServer对每个请求的处理都是一个协程来运行。很多Go项目上线时出现意外,基本上都和Goroutines有关。大家将充当救火队长,抢着查看指标和日志,通过PProf收集Goroutine运行状态。自然,他也是最受关注的“明星”,所以在日常采访中,被问到的概率极高。GoroutineLeaks了解了大家爱问的原因后,我们开始研究Goroutine泄漏的N种方法,希望通过前人留下的“坑”,了解它们的原理,避免这些问题。造成泄露的原因大部分是:channel/mutex等读写操作在Goroutine中进行,但是由于逻辑问题,在某些情况下会一直阻塞。Goroutine中的业务逻辑进入死循环,无法释放资源。Goroutine中的业务逻辑等待的时间很长,不断有新的Goroutine进入等待。下面引用一些网上搜集的Goroutine泄露的例子(文末会注明出处)。通道使用不当Goroutine+Channel是最经典的组合,所以这里出现了很多泄漏。最经典的就是上面提到的channel进行读写操作时的逻辑问题。发送但不接收第一个例子:funcmain(){fori:=0;我<4;i++{queryAll()fmt.Printf("goroutines:%d\n",runtime.NumGoroutine())}}funcqueryAll()int{ch:=make(chanint)fori:=0;我<3;i++{gofunc(){ch<-query()}()}return<-ch}funcquery()int{n:=rand.Intn(100)time.Sleep(time.Duration(n)*time.Millisecond)returnn}outputresult:goroutines:3goroutines:5goroutines:7goroutines:9本例中我们多次调用queryAll方法,在for循环中使用Goroutine调用query方法。关键是调用查询方法后的结果会写入ch变量,接收成功后会返回ch变量。最后可以看到输出的goroutine数量在不断增加,每次增加2个。即每次调用时,Goroutine都会被泄露。原因是channel已经发送了(每次3个),但是接收端还没有完全接收到(只返回1个ch),从而诱发了Goroutineleak。接收而不发送第二个例子:}{}}()time.Sleep(time.Second)}outputresult:goroutines:2在这个例子中,两者是相对于“发送和不接收”,通道接收值,但如果不发送,也会造成堵塞。但是在实际的业务场景中,一般会比较复杂。基本上,在很多业务逻辑中,如果某个通道的读写操作出现问题,自然会被阻塞。nilchannel第三个例子:funcmain(){deferfunc(){fmt.Println("goroutines:",runtime.NumGoroutine())}()varchchanintgofunc(){<-ch}()时间。Sleep(time.Second)}outputresult:goroutines:2在这个例子中可以知道,如果channel忘记初始化,无论是读还是写都会造成阻塞。正常的初始化姿势是:ch:=make(chanint)gofunc(){<-ch}()ch<-0time.Sleep(time.Second)调用make函数进行初始化。奇怪的慢等待第四个例子:funcmain(){for{gofunc(){_,err:=http.Get("https://www.xxx.com/")iferr!=nil{fmt.Printf("http.Geterr:%v\n",err)}//做点什么...}()time.Sleep(time.Second*1)fmt.Println("goroutines:",runtime.NumGoroutine())}}Output:goroutines:5goroutines:9goroutines:13goroutines:17goroutines:21goroutines:25...在这个例子中,展示了Go语言中一个经典的事故场景。即一般我们会在应用程序中调用第三方服务的接口。但是第三方接口有时候很慢,很长时间没有返回响应结果。正好Go语言默认的http.Client没有设置超时时间。所以会导致一直堵,一直堵着就一直爽。自然而然,Goroutine会不断暴涨,不断泄漏,最终占用资源,引发事故。在Go项目中,我们一般建议至少给http.Client设置一个超时时间:httpClient:=http.Client{Timeout:time.Second*15,}并采取限流熔断等措施,防止突发流量造成依赖崩溃,还是吃P0。Mutex忘记解锁第五个例子:funcmain(){total:=0deferfunc(){time.Sleep(time.Second)fmt.Println("total:",total)fmt.Println("goroutines:",runtime.NumGoroutine())}()varmutexsync.Mutexfori:=0;我<10;i++{gofunc(){mutex.Lock()total+=1}()}}outputresult:total:1goroutines:10在这个例子中,第一个mutexsync.Mutex被锁住了,但是他可能在处理业务逻辑,或者他可能忘记解锁。结果后面所有的sync.Mutex都想加锁,但是因为没有释放而被阻塞了。一般在Go项目中,我们建议这样写:varmutexsync.Mutexfori:=0;我<10;i++{gofunc(){mutex.Lock()defermutex.Unlock()total+=1}()}同步锁使用不当第六个例子:funchandle(vint){varwgsync.WaitGroupwg.Add(5)对于i:=0;我