如何優雅的使用Goroutine上-Goroutine怎麼洩漏和排查

Go從語言層面就簡化了併發程式設計,從而降低併發程式設計的難度,但這並不意味著我們可以隨意使用Go關鍵字建立Goroutine。

之前接手一個下載服務,一個http請求內部開了好幾個Goroutine還沒有超時處理,大量的Groutine洩漏程式碼,各種秀程式碼,程式碼時寫給人看的,越簡單越好。接下來我們一起看看怎麼優雅的使用 Goroutine

1。 幾種Goroutine常見的洩漏 1。1。channel讀寫操作堵塞,由於邏輯問題導致一直堵塞 1。2。Mutex/RWMutex 互斥鎖、讀寫鎖,由於邏輯問題導致一直堵塞 1。3。goroutine 內的邏輯死迴圈 1。4。goroutine 內的邏輯進入超長時間等待(比如呼叫第三方介面、庫、cgo等沒有設定超時控制)2。 怎麼避免Goroutine洩漏3。 怎麼排查Goroutine洩漏4。 管控Goroutine生命週期

1. 幾種Goroutine常見的洩漏

1。1。channel讀寫操作堵塞,由於邏輯問題導致一直堵塞1。2。Mutex/RWMutex 互斥鎖、讀寫鎖,由於邏輯問題導致一直堵塞1。3。goroutine 內的邏輯死迴圈1。4。goroutine 內的邏輯進入超長時間等待(比如呼叫第三方介面、庫、cgo等沒有設定超時控制)

1.channel讀寫操作堵塞,由於邏輯問題導致一直堵塞

示例1:

package mainimport (“fmt”“runtime”“time”)func search(name string) chan<- string { // ch是無緩衝通道,導致這個 goroutine 沒人接收導致一直堵塞等待 ch := make(chan string) go func() { fmt。Println(name, “send before”) ch <- name // 一直會堵塞在這裡,導致後面程式碼無法執行 fmt。Println(name, “send done”) }() return ch}func main() { for i := 1; i <= 3; i++ { go func(index int) { name := fmt。Sprintf(“gourtine %d”, index) search(name) }(i) fmt。Printf(“goroutine count: %d\n”, runtime。NumGoroutine()) } time。Sleep(time。Second * 2) fmt。Printf(“end goroutine count: %d\n”, runtime。NumGoroutine())}/**輸出:goroutine count: 2goroutine count: 3goroutine count: 4gourtine 1 send beforegourtine 3 send beforegourtine 2 send beforeend goroutine count: 4 */

程式結束的時候還有4個goroutine,有3個還在堵塞等待有人從通道中接收資料,導致Goroutine洩漏。有人說很簡單變成有緩衝通道就好,我把第11行程式碼加個1就好了。

ch := make(chan string, 1)

修改以後執行結果如下:

goroutine count: 2goroutine count: 3goroutine count: 4gourtine 3 send beforegourtine 3 send donegourtine 1 send beforegourtine 1 send donegourtine 2 send beforegourtine 2 send doneend goroutine count: 1

雖然解決了問題,還是仍然不夠好。

我們又修改了版本2程式碼如下:

package mainimport ( “context” “fmt” “runtime” “time”)// context 應該 search 函式外面傳入,這裡為了演示方便,我們後面說怎麼優雅的使用func search(name string) chan<- string { ch := make(chan string) go func() { // 這個 goroutine 加上了超時控制 // 要麼有接收通道資料 或者 100毫秒後超時返回 ctx, cancel := context。WithTimeout(context。Background(), time。Microsecond * 100) defer cancel() fmt。Println(name, “send before”) select { case ch <- name: fmt。Println(name, “send done”) case <-ctx。Done(): fmt。Println(name, “send timeout”) } }() return ch}func main() { for i := 1; i <= 3; i++ { go func(index int) { name := fmt。Sprintf(“gourtine %d”, index) search(name) }(i) fmt。Printf(“goroutine count: %d\n”, runtime。NumGoroutine()) } time。Sleep(time。Second * 5) fmt。Printf(“end goroutine count: %d\n”, runtime。NumGoroutine())}/**goroutine count: 2goroutine count: 3goroutine count: 4gourtine 3 send beforegourtine 2 send beforegourtine 1 send beforegourtine 3 send timeoutgourtine 2 send timeoutgourtine 1 send timeoutend goroutine count: 1*/

程式結束的時候只有1個goroutine,因為3個工作的Goroutine等了100毫秒,沒人接收,就超時返回了。我們一般在一個高頻的http、rpc 請求都會做超時處理,要麼降級處理返回熱點資料,要麼報錯。

2.Mutex/RWMutex 互斥鎖、讀寫鎖,由於邏輯問題導致一直堵塞

示例2.1:

Goroutine加鎖不解鎖就退出了,導致後續goroutine一直堵塞

package mainimport( “fmt” “runtime” “sync” “time”)var mutex sync。Mutexfunc main() { for i := 1; i <= 3; i ++ { go func(index int) { name := fmt。Sprintf(“goroutine %d”, index) defer fmt。Println(name, “ defer”) fmt。Println(name, “ lock before”) // 第一個獲取到鎖的goroutine沒有解鎖就退出了,導致後續goroutine一直堵塞在這裡 mutex。Lock() fmt。Println(name, “ locked”) }(i) fmt。Println(“goroutine count: ”, runtime。NumGoroutine()) } time。Sleep(time。Second * 2) fmt。Println(“goroutine count: ”, runtime。NumGoroutine())}/**goroutine count: 2goroutine count: 3goroutine count: 4goroutine 2 lock beforegoroutine 2 lockedgoroutine 2 defergoroutine 1 lock beforegoroutine 3 lock beforegoroutine count: 3*/

goroutine2 搶到了鎖,但是它退出的時候沒有解鎖,導致goroutine1和goroutine3到程式結束的時候還在獲取鎖,一直堵塞著,導致goroutine洩露。我們在加鎖後面增加“defer mutex。Unlock()”,後執行發現沒有出現洩露

goroutine count: 2goroutine count: 3goroutine count: 4goroutine 1 lock beforegoroutine 1 lockedgoroutine 1 defergoroutine 2 lock beforegoroutine 2 lockedgoroutine 2 defergoroutine 3 lock beforegoroutine 3 lockedgoroutine 3 defergoroutine count: 1

示例2.2:

Goroutine加讀鎖沒有解鎖,就去加寫鎖。導致後續 Goroutine 在加讀鎖的時候會堵塞

package mainimport ( “runtime” “sync” “fmt” “time”)var rwMutex sync。RWMutexfunc main() { for i := 1; i <= 3; i ++ { go func(index int) { name := fmt。Sprintf(“goroutine %d”, index) defer fmt。Println(name, “ defer”) fmt。Println(name, “ RLock before”) // 第一個拿到讀鎖goroutine 沒有解鎖,就去加寫鎖。導致後續 goroutine 在加 讀鎖的時候會堵塞 rwMutex。RLock() fmt。Println(name, “ RLock locked”) rwMutex。Lock() fmt。Println(name, “ locked”) }(i) fmt。Println(“goroutine count: ”, runtime。NumGoroutine()) } time。Sleep(time。Second * 2) fmt。Println(“goroutine count: ”, runtime。NumGoroutine())}/**goroutine count: 2goroutine count: 3goroutine count: 4goroutine 3 RLock beforegoroutine 3 RLock lockedgoroutine 1 RLock beforegoroutine 2 RLock beforegoroutine count: 4 */

goroutine3先拿到讀鎖,沒有解鎖就去加寫鎖,導致goroutine1和goroutine2在加讀鎖的時候一直獲取不到堵塞,導致goroutine洩露。

3.goroutine 內的邏輯死迴圈

示例3:

package mainimport( “fmt” “runtime” “sync” “time”)var mutex sync。Mutexfunc main() { for i := 1; i <= 3; i ++ { go func(index int) { name := fmt。Sprintf(“goroutine %d”, index) for { fmt。Println(name, “ running”) time。Sleep(time。Millisecond * 900) } }(i) fmt。Println(“goroutine count: ”, runtime。NumGoroutine()) } time。Sleep(time。Second * 2) fmt。Println(“goroutine count: ”, runtime。NumGoroutine())}/**goroutine count: 2goroutine count: 3goroutine count: 4goroutine 3 runninggoroutine 1 runninggoroutine 2 runninggoroutine 2 runninggoroutine 1 runninggoroutine 3 runninggoroutine 3 runninggoroutine 1 runninggoroutine 2 runninggoroutine count: 4*/

4.goroutine 內的邏輯進入超長時間等待(比如呼叫第三方介面、庫、cgo等沒有設定超時控制)

示例4:

package mainimport ( “fmt” “net/http” “runtime” “time”)func search(name string) { // 用不存在域名,模擬請求第三方介面超長時間等待 res, err := http。Get(“http://notfound。notfound。com”) if err != nil { fmt。Println(name, “ err:”, err) return } defer res。Body。Close() fmt。Println(name, “ ok”)}func main() { for i := 1; i <= 3; i++ { go func(index int) { name := fmt。Sprintf(“gourtine %d”, index) search(name) }(i) fmt。Printf(“goroutine count: %d\n”, runtime。NumGoroutine()) } time。Sleep(time。Second * 2) fmt。Printf(“end goroutine count: %d\n”, runtime。NumGoroutine())}/**輸出:goroutine count: 2goroutine count: 3goroutine count: 4end goroutine count: 10*/

在呼叫第三方介面的時候,由於介面很慢,很久不返回結果。http。client預設是沒有設定超時時間,導致Goroutine洩漏。所以在呼叫第三方介面的時候我們要做好超時控制。

2. 怎麼避免Goroutine洩漏

1。避免channel堵塞(在寫channel入方關閉,不關閉也沒事,gc會自動回收)2。避免死鎖堵塞(同類型的鎖成對出現,固定順序鎖)3。避免Goroutine內部邏輯死迴圈4。不知道一個資源或者函式多久返回時,要做好超時控制5。本質就是管控Goroutine生命週期

3. 怎麼排查Goroutine洩漏

一般我們使用 go tool & Graph & 火焰圖

& pprof 來排查 Goroutine洩漏,pprof結合業務程式碼確定是否洩漏。可以透過火焰圖,看的那些函式佔用的比較多,來排查。具體的使用請方式自行百度。

4. 管控Goroutine生命週期

只有把Groutine的生命週期管理起來,才能避免Goroutine洩漏。那怎麼管控Goroutine的生命週期,我們一起來看這篇文章如何管控Goroutine的生命週期