回答我,停止 Goroutine 有幾種方法?
大家好,我是煎魚。
協(xié)程(goroutine)作為 Go 語言的扛把子,經(jīng)常在各種 Go 工程項目中頻繁露面,甚至有人會為了用 goroutine 而強行用他。
在 Go 工程師的面試中,也繞不開他,會有人問 ”如何停止一個 goroutine?”,一下子就把話題范圍擴大了,這是一個涉及多個知識點的話題,能進一步深入問。
為此,今天煎魚就帶大家了解一下停止 goroutine 的方法!
goroutine 案例
在日常的工作中,我們常會有這樣的 Go 代碼,go 關鍵字一把搜起一個 goroutine:
func?main()?{?
?ch?:=?make(chan?string,?6)
?go?func()?{
??for?{
???ch?<-?"腦子進煎魚了"
??}
?}()
}
初入 goroutine 大門的開發(fā)者可能就完事了,但跑一段時間后,他就可能會遇到一些問題,苦苦排查...
像是:當 goroutine 內(nèi)的任務,運行的太久,又或是卡死了...就會一直阻塞在系統(tǒng)中,變成 goroutine 泄露,或是間接造成資源暴漲,會帶來許多的問題。
如何在停止 goroutine,就成了一門必修技能了,不懂就沒法用好 goroutine。
關閉 channel
第一種方法,就是借助 channel 的 close 機制來完成對 goroutine 的精確控制。
代碼如下:
func?main()?{
?ch?:=?make(chan?string,?6)
?go?func()?{
??for?{
???v,?ok?:=?<-ch
???if?!ok?{
????fmt.Println("結(jié)束")
????return
???}
???fmt.Println(v)
??}
?}()
?ch?<-?"煎魚還沒進鍋里..."
?ch?<-?"煎魚進腦子里了!"
?close(ch)
?time.Sleep(time.Second)
}
在 Go 語言的 channel 中,channel 接受數(shù)據(jù)有兩種方法:
msg?:=?<-ch
msg,?ok?:=?<-ch
這兩種方式對應著不同的 runtime 方法,我們可以利用其第二個參數(shù)進行判別,當關閉 channel 時,就根據(jù)其返回結(jié)果跳出。
另外我們也可以利用 for range 的特性:
?go?func()?{
??for?{
???for?v?:=?range?ch?{
????fmt.Println(v)
???}
??}
?}()
其會一直循環(huán)遍歷通道 ch,直到其關閉為止,是頗為常見的一種用法。
定期輪詢 channel
第二種方法,是更為精細的方法,其結(jié)合了第一種方法和類似信號量的處理方式。
代碼如下:
func?main()?{
?ch?:=?make(chan?string,?6)
?done?:=?make(chan?struct{})
?go?func()?{
??for?{
???select?{
???case?ch?<-?"腦子進煎魚了":
???case?<-done:
????close(ch)
????return
???}
???time.Sleep(100?*?time.Millisecond)
??}
?}()
?go?func()?{
??time.Sleep(3?*?time.Second)
??done?<-?struct{}{}
?}()
?for?i?:=?range?ch?{
??fmt.Println("接收到的值:?",?i)
?}
?fmt.Println("結(jié)束")
}
在上述代碼中,我們聲明了變量 done,其類型為 channel,用于作為信號量處理 goroutine 的關閉。
而 goroutine 的關閉是不知道什么時候發(fā)生的,因此在 Go 語言中會利用 for-loop 結(jié)合 select 關鍵字進行監(jiān)聽,再進行完畢相關的業(yè)務處理后,再調(diào)用 close 方法正式關閉 channel。
若程序邏輯比較簡單結(jié)構(gòu)化,也可以不調(diào)用 close 方法,因為 goroutine 會自然結(jié)束,也就不需要手動關閉了。
使用 context
第三種方法,可以借助 Go 語言的上下文(context)來做 goroutine 的控制和關閉。
代碼如下:
func?main()?{
?ch?:=?make(chan?struct{})
?ctx,?cancel?:=?context.WithCancel(context.Background())
?go?func(ctx?context.Context)?{
??for?{
???select?{
???case?<-ctx.Done():
????ch?<-?struct{}{}
????return
???default:
????fmt.Println("煎魚還沒到鍋里...")
???}
???time.Sleep(500?*?time.Millisecond)
??}
?}(ctx)
?go?func()?{
??time.Sleep(3?*?time.Second)
??cancel()
?}()
?<-ch
?fmt.Println("結(jié)束")
}
在 context 中,我們可以借助 ctx.Done 獲取一個只讀的 channel,類型為結(jié)構(gòu)體。可用于識別當前 channel 是否已經(jīng)被關閉,其原因可能是到期,也可能是被取消了。
因此 context 對于跨 goroutine 控制有自己的靈活之處,可以調(diào)用 context.WithTimeout 來根據(jù)時間控制,也可以自己主動地調(diào)用 cancel 方法來手動關閉。
干掉另外一個 goroutine
在了解了停止 goroutine 的 3 種經(jīng)典方法后,又有小伙伴提出了新的想法。就是 “我想在 goroutineA 里去停止 goroutineB,有辦法嗎?”
答案是不能,因為在 Go 語言中,goroutine 只能自己主動退出,一般通過 channel 來控制,不能被外界的其他 goroutine 關閉或干掉,也沒有 goroutine 句柄的顯式概念。

在 Go issues 中也有人提過類似問題,Dave Cheney 給出了一些思考:
如果一個 goroutine 被強行停止了,它所擁有的資源會發(fā)生什么?堆棧被解開了嗎?defer 是否被執(zhí)行? 如果執(zhí)行 defer,該 goroutine 可能可以繼續(xù)無限期地生存下去。 如果不執(zhí)行 defer,該 goroutine 原本的應用程序系統(tǒng)設計邏輯將會被破壞,這肯定不合理。 如果允許強制停止 goroutine,是要釋放所有東西,還是直接把它從調(diào)度器中踢出去,你想通過此解決什么問題?
這都是值得深思的,另外一旦放開這種限制。作為程序員,你維護代碼。很有可能就不知道 goroutine 的句柄被傳到了哪里,又是在何時何地被人莫名其妙關閉,非常糟糕...
總結(jié)
在今天這篇文章中,我們介紹了在 Go 語言中停止 goroutine 的三大經(jīng)典方法(channel、context,channel+context)和其背后的使用原理。
同時針對 goroutine 不可以跨 goroutine 強制停止的原因進行了分析。其實 goroutine 的設計就是這樣的,包括像 goroutine+panic+recover 的設計也是遵循這個原理,因此也有的 Go 開發(fā)者總是會誤以為跨 goroutine 能有 recover 接住...
記住,在 Go 語言中每一個 goroutine 都需要自己承擔自己的任何責任,這是基本原則。
(你已經(jīng)是個成熟的 goroutine 了...)
關注煎魚,吸取他的知識???

你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎,大學搞過 PHP。現(xiàn)在整 Go,在公司負責微服務架構(gòu)等相關工作推進和研發(fā)。
從大學開始靠自己賺生活費和學費,到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽,點擊藍字查看我的出書之路。
日常分享高質(zhì)量文章,輸出 Go 面試、工作經(jīng)驗、架構(gòu)設計,加微信拉讀者交流群,記得點贊!
