微信搜索【 脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有个人系列文章、资料和开源 Go 图书。
你们好,我是煎鱼。git
前几天分享 Go 群友提问的文章时,有读者在朋友圈下提到,但愿我可以针对 Goroutine 泄露这块进行讲解,他在面试的时候常常被问到。github
今天的男主角,就是 Go 语言的著名品牌标识 Goroutine,一个随随便便就能开几十万个快车进车道的大杀器。golang
for { go func() {}() }
本文会聚焦于 Goroutine 泄露的 N 种方法,进行详解和说明。面试
面试官为啥会问 Goroutine(协程)泄露这种奇特的问题呢?算法
能够猜想是:微信
不少 Go 工程在线上出事故时,基本 Goroutine 的关联,你们都会做为救火队长,风风火火的跑去看指标、看日志,经过 PProf 采集 Goroutine 运行状况等。并发
天然他也就是最受瞩目的那颗 “星” 了,因此在平常面试中,被问概率也就极高了。函数
了解清楚你们爱问的缘由后,咱们开始对 Goroutine 泄露的 N 种方法进行研究,但愿经过前人留下的 “坑”,了解其原理和避开这些问题。性能
泄露的缘由大多集中在:测试
接下来我会引用在网上冲浪收集到的一些 Goroutine 泄露例子(会在文末参考注明出处)。
Goroutine+Channel 是最经典的组合,所以很多泄露都出现于此。
最经典的就是上面提到的 channel 进行读写操做时的逻辑问题。
第一个例子:
func main() { for i := 0; i < 4; i++ { queryAll() fmt.Printf("goroutines: %d\n", runtime.NumGoroutine()) } } func queryAll() int { ch := make(chan int) for i := 0; i < 3; i++ { go func() { ch <- query() }() } return <-ch } func query() int { n := rand.Intn(100) time.Sleep(time.Duration(n) * time.Millisecond) return n }
输出结果:
goroutines: 3 goroutines: 5 goroutines: 7 goroutines: 9
在这个例子中,咱们调用了屡次 queryAll
方法,并在 for
循环中利用 Goroutine 调用了 query
方法。其重点在于调用 query
方法后的结果会写入 ch
变量中,接收成功后再返回 ch
变量。
最后可看到输出的 goroutines 数量是在不断增长的,每次多 2 个。也就是每调用一次,都会泄露 Goroutine。
缘由在于 channel 均已经发送了(每次发送 3 个),可是在接收端并无接收彻底(只返回 1 个 ch),所诱发的 Goroutine 泄露。
第二个例子:
func main() { defer func() { fmt.Println("goroutines: ", runtime.NumGoroutine()) }() var ch chan struct{} go func() { ch <- struct{}{} }() time.Sleep(time.Second) }
输出结果:
goroutines: 2
在这个例子中,与 “发送不接收” 二者是相对的,channel 接收了值,可是不发送的话,一样会形成阻塞。
但在实际业务场景中,通常更复杂。基本是一大堆业务逻辑里,有一个 channel 的读写操做出现了问题,天然就阻塞了。
第三个例子:
func main() { defer func() { fmt.Println("goroutines: ", runtime.NumGoroutine()) }() var ch chan int go func() { <-ch }() time.Sleep(time.Second) }
输出结果:
goroutines: 2
在这个例子中,能够得知 channel 若是忘记初始化,那么不管你是读,仍是写操做,都会形成阻塞。
正常的初始化姿式是:
ch := make(chan int) go func() { <-ch }() ch <- 0 time.Sleep(time.Second)
调用 make
函数进行初始化。
第四个例子:
func main() { for { go func() { _, err := http.Get("https://www.xxx.com/") if err != nil { fmt.Printf("http.Get err: %v\n", err) } // do something... }() time.Sleep(time.Second * 1) fmt.Println("goroutines: ", runtime.NumGoroutine()) } }
输出结果:
goroutines: 5 goroutines: 9 goroutines: 13 goroutines: 17 goroutines: 21 goroutines: 25 ...
在这个例子中,展现了一个 Go 语言中经典的事故场景。也就是通常咱们会在应用程序中去调用第三方服务的接口。
可是第三方接口,有时候会很慢,久久不返回响应结果。刚好,Go 语言中默认的 http.Client
是没有设置超时时间的。
所以就会致使一直阻塞,一直阻塞就一直爽,Goroutine 天然也就持续暴涨,不断泄露,最终占满资源,致使事故。
在 Go 工程中,咱们通常建议至少对 http.Client
设置超时时间:
httpClient := http.Client{ Timeout: time.Second * 15, }
而且要作限流、熔断等措施,以防突发流量形成依赖崩塌,依然吃 P0。
第五个例子:
func main() { total := 0 defer func() { time.Sleep(time.Second) fmt.Println("total: ", total) fmt.Println("goroutines: ", runtime.NumGoroutine()) }() var mutex sync.Mutex for i := 0; i < 10; i++ { go func() { mutex.Lock() total += 1 }() } }
输出结果:
total: 1 goroutines: 10
在这个例子中,第一个互斥锁 sync.Mutex
加锁了,可是他可能在处理业务逻辑,又或是忘记 Unlock
了。
所以致使后面的全部 sync.Mutex
想加锁,却因未释放又都阻塞住了。通常在 Go 工程中,咱们建议以下写法:
var mutex sync.Mutex for i := 0; i < 10; i++ { go func() { mutex.Lock() defer mutex.Unlock() total += 1 }() }
第六个例子:
func handle(v int) { var wg sync.WaitGroup wg.Add(5) for i := 0; i < v; i++ { fmt.Println("脑子进煎鱼了") wg.Done() } wg.Wait() } func main() { defer func() { fmt.Println("goroutines: ", runtime.NumGoroutine()) }() go handle(3) time.Sleep(time.Second) }
在这个例子中,咱们调用了同步编排 sync.WaitGroup
,模拟了一遍咱们会从外部传入循环遍历的控制变量。
但因为 wg.Add
的数量与 wg.Done
数量并不匹配,所以在调用 wg.Wait
方法后一直阻塞等待。
在 Go 工程中使用,咱们会建议以下写法:
var wg sync.WaitGroup for i := 0; i < v; i++ { wg.Add(1) defer wg.Done() fmt.Println("脑子进煎鱼了") } wg.Wait()
咱们能够调用 runtime.NumGoroutine
方法来获取 Goroutine 的运行数量,进行先后一比较,就能知道有没有泄露了。
但在业务服务的运行场景中,Goroutine 内致使的泄露,大多数处于生产、测试环境,所以更多的是使用 PProf:
import ( "net/http" _ "net/http/pprof" ) http.ListenAndServe("localhost:6060", nil))
只要咱们调用 http://localhost:6060/debug/pprof/goroutine?debug=1
,PProf 会返回全部带有堆栈跟踪的 Goroutine 列表。
也能够利用 PProf 的其余特性进行综合查看和分析,这块参考我以前写的《Go 大杀器之性能剖析 PProf》,基本是全村最全的教程了。
在今天这篇文章中,咱们针对 Goroutine 泄露的 N 种常见的方式方法进行了一一分析,虽然说看起来都是比较基础的场景。
但结合在实际业务代码中,就是一大坨中的某个细节致使全盘皆输了,但愿上面几个案例可以给你们带来警戒。
而面试官爱问,怕不是本身踩过许多坑,也但愿进来的同僚,也是身经百战了。
靠谱的工程师,而非只是八股工程师。
如有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创做的最大动力,感谢支持。
文章持续更新,能够微信搜【脑子进煎鱼了】阅读,回复【 000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blog 已收录,欢迎 Star 催更。