若是你啓動了一個 goroutine,但並無符合預期的退出,直到程序結束,此goroutine才退出,這種狀況就是 goroutine 泄露。當 goroutine 泄露發生時,該 goroutine 的棧(通常 2k 內存空間起)一直被佔用不能釋放,goroutine 裏的函數在堆上申請的空間也不能被 垃圾回收器 回收。這樣,在程序運行期間,內存佔用持續升高,可用內存越來也少,最終將致使系統崩潰。html
回顧一下 goroutine 終止的場景:segmentfault
那麼當這三者同時沒發生的時候,就會致使 goroutine 始終不會終止退出。tcp
goroutine泄露通常是由於channel操做阻塞而致使整個routine一直阻塞等待或者 goroutine 裏有死循環的時候。能夠細分爲下面五種狀況:函數
// leak 是一個有 bug 程序。它啓動了一個 goroutine 阻塞接收 channel。當 Goroutine 正在等待時,leak 函數會結束返回。此時,程序的其餘任何部分都不能經過 channel 發送數據,那個 channel 永遠不會關閉,fmt.Println 調用永遠不會發生, 那個 goroutine 會被永遠鎖死 func leak() { ch := make(chan int) go func() { val := <-ch fmt.Println("We received a value:", val) }() }
// 一個複雜一點的例子 func sendMsg(msg, addr string) error { conn, err := net.Dial("tcp", addr) if err != nil { return err } defer conn.Close() _, err = fmt.Fprint(conn, msg) return err } func broadcastMsg(msg string, addrs []string) error { errc := make(chan error) for _, addr := range addrs { go func(addr string) { errc <- sendMsg(msg, addr) fmt.Println("done") }(addr) } for _ = range addrs { if err := <-errc; err != nil { return err } } return nil } func main() { addr := []string{"localhost:8080", "http://google.com"} err := broadcastMsg("hi", addr) time.Sleep(time.Second) if err != nil { fmt.Println(err) return } fmt.Println("everything went fine") }
對於 broadcastMsg 裏的這一段工具
for _ = range addrs { if err := <-errc; err != nil { return err } }
當遇到 第一條不爲 nil 的 err,broadcastMsg就返回了,那麼從第二個調用 sendMsg 後返回值 err 不爲 nil 的 goroutine 在 errc <- sendMsg(msg, addr)
這裏都將阻塞而形成這些 goroutine 不能退出。ui
和第二種狀況比較相似。google
在 channel 的接收值數量有限,且能夠用 buffered channel 的狀況下,那 buffer size 就分配的和 接收值數量 同樣,這樣能夠解決掉第二、3種緣由形成的泄露。好比在第二種中,改爲spa
errc := make(chan error, len(addrs))
問題就解決了。code
注意:time package裏的定時器使用不當也會形成 goroutine 泄露。協程
tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) select { case <-tick: // Do nothing. case <-abort: fmt.Println("aborted!") return } }
以上的代碼中,當 for 循環結束後,tick 將再也不有接收者,time.Tick 啓動的 goroutine 將產生泄露。
建議在程序的整個生命週期須要 ticks 時才使用 time.Tick,不然建議按以下模式使用:
ticker := time.NewTicker(1 * time.Second) <- ticker.C ticker.Stop() // 當再也不使用後,結束 ticker 的 goroutine
實現一個 fibonacci 數列生成器,並在獨立的 goroutine 中運行,在讀取完須要長度的數列後,若是 用於 退出生成器的 quit 忘了被 close (或寫入數據),select 將一直被阻塞形成 該 goroutine 泄露。
func fibonacci(c, quit chan int) { x, y := 0, 1 for{ select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go fibonacci(c, quit) for i := 0; i < 10; i++{ fmt.Println(<- c) } // close(quit) }
在這種須要一個獨立的 goroutine 做爲生成器的場景下,爲了能在外部結束這個 goroutine,咱們一般有兩種方法:
func fibonacci(c chan int, ctx context.Context) { x, y := 0, 1 for{ select { case c <- x: x, y = y, x+y case <-ctx.Done(): fmt.Println("quit") return } } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := make(chan int) go fibonacci(c, ctx) for i := 0; i < 10; i++{ fmt.Println(<- c) } cancel() time.Sleep(5 * time.Second) }
一般因爲代碼裏循環的退出條件實現的不對,致使死循環。
// 粗暴的示例 func foo() { for{ fmt.Println("fooo") } }
這篇文章 有關於檢測和定位更詳細的描述,能夠參考,本處再也不累述。
參考:
https://segmentfault.com/a/11...
https://www.ardanlabs.com/blo...