goroutine泄露:原理、場景、檢測和防範

若是你啓動了一個 goroutine,但並無符合預期的退出,直到程序結束,此goroutine才退出,這種狀況就是 goroutine 泄露。當 goroutine 泄露發生時,該 goroutine 的棧(通常 2k 內存空間起)一直被佔用不能釋放,goroutine 裏的函數在堆上申請的空間也不能被 垃圾回收器 回收。這樣,在程序運行期間,內存佔用持續升高,可用內存越來也少,最終將致使系統崩潰。html

回顧一下 goroutine 終止的場景:segmentfault

  • 當一個goroutine完成它的工做
  • 因爲發生了沒有處理的錯誤
  • 有其餘的協程告訴它終止

那麼當這三者同時沒發生的時候,就會致使 goroutine 始終不會終止退出。tcp

goroutine 泄露的場景

goroutine泄露通常是由於channel操做阻塞而致使整個routine一直阻塞等待或者 goroutine 裏有死循環的時候。能夠細分爲下面五種狀況:函數

1. 從 channel 裏讀,可是沒有寫

// 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)
    }()
}

2. 向 unbuffered channel 寫,可是沒有讀

// 一個複雜一點的例子
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

3. 向已滿的 buffered channel 寫,可是沒有讀

和第二種狀況比較相似。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

4. select操做在全部case上阻塞

實現一個 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,咱們一般有兩種方法:

  • 使用上述實現裏的模式,傳入一個 quit channel,配合 select,當不須要的時候,close 這個 quit channel,該 goroutine 就能夠退出。
  • 使用 context 包:
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)
}

5. goroutine進入死循環中,致使資源一直沒法釋放

一般因爲代碼裏循環的退出條件實現的不對,致使死循環。

// 粗暴的示例
func foo() {
  for{
    fmt.Println("fooo")
  }
}

goroutine 泄露檢測和定位

  1. 監控工具:固定週期對進程的內存佔用狀況進行採樣,數據可視化後,根據內存佔用走勢(持續上升),很容易發現是否發生內存泄露。可使用雲服務提供的內存使用監控服務或者本身實現一個 daemon 腳本週期採集內存佔用數據。
  2. 使用Go提供的pprof工具分析是否發生內存泄露。使用 pprof 的 heap 可以獲取程序運行時的內存信息,經過對運行的程序屢次採樣對比,分析出內存的使用狀況。

這篇文章 有關於檢測和定位更詳細的描述,能夠參考,本處再也不累述。

goroutine 泄露的防範

  • 建立goroutine時就要想好該goroutine該如何結束
  • 使用channel時,要考慮到 channel 阻塞時協程可能的行爲
  • 實現循環語句時注意循環的退出條件,避免死循環

參考:

https://segmentfault.com/a/11...

https://www.ardanlabs.com/blo...

圖片描述

相關文章
相關標籤/搜索