Go 防止 goroutine 洩露的方法

2022-09-28 09:24:08 字數 3828 閱讀 9978

概述

go 的併發模型與其他語言不同,雖說它簡化了併發程式的開發難度,但如果不了解使用方法,常常會遇到 goroutine 洩露的問題。雖然 goroutine 是輕量級的執行緒,占用資源很少,但如果一直得不到釋放並且還在不斷建立新協程,毫無疑問是有問題的,並且是要在程式執行幾天,甚至更長的時間才能發現的問題。

對於上面描述的問題,我覺得可以從兩方面入手解決,如下:

一是預防,要做到預防,我們就需要了解什麼樣的**會產生洩露,以及了解如何寫出正確的**;

二是監控,雖說預防減少了洩露產生的概率,但沒有人敢說自己不犯錯,因而,通常我們還需要一些監控手段進一步保證程式的健壯性;

接下來,我將會分兩篇文章分別從這兩個角度進行介紹,今天先談第一點。

如何監控洩露

本文主要集中在第一點上,但為了更好的演示效果,可以先介紹乙個最簡單的監控方式。通過 runtime.numgoroutine() 獲取當前執行中的 goroutine 數量,通過它確認是否發生洩漏。它的使用非常簡單,就不為它專門寫個例子了。

乙個簡單的例子

語言級別的併發支援是 go 的一大優勢,但這個優勢也很容易被濫用。通常我們在開始 go 併發學習時,常常聽別人說,go 的併發非常簡單,在呼叫函式前加上 go 關鍵詞便可啟動 goroutine,即乙個併發單元,但很多人可能只聽到了這句話,然後就出現了類似下面的**:

package main

import (

"fmt"

"runtime"

"time"

)func sayhello()

}func main() ()

go sayhello()

fmt.println("hello main")

}對 go 比較熟悉的話,很容易發現這段**的問題,sayhello 是個死迴圈,沒有如何退出機制,因此也就沒有任何辦法釋放建立的 goroutine。我們通過在 main 函式最前面的 defer 實現在函式退出時列印當前執行中的 goroutine 數量,毫無意外,它的輸出如下:

the number of goroutines: 2

不過,因為上面的程式並非常駐,有洩露問題也不大,程式退出後系統會自動**執行時資源。但如果這段**在常駐服務中執行,比如 http server,每接收到乙個請求,便會啟動一次 sayhello,時間流逝,每次啟動的 goroutine 都得不到釋放,你的服務將會離奔潰越來越近。

這個例子比較簡單,我相信,對 go 的併發稍微有點了解的朋友都不會犯這個錯。

洩露情況分類

前面介紹的例子由於在 goroutine 執行死迴圈導致的洩露。接下來,我會按照併發的資料同步方式對洩露的各種情況進行分析。簡單可歸於兩類,即:

傳統同步機制主要指面向共享記憶體的同步機制,比如排它鎖、共享鎖等。這兩種情況導致的洩露還是比較常見的。go 由於 defer 的存在,第二類情況,一般情況下還是比較容易避免的。

chanel 引起的洩露

先說 channel,如果之前讀過官方的那篇併發的文章[1],翻譯版[2],你會發現 channel 的使用,乙個不小心就洩露了。我們來具體總結下那些情況下可能導致。

傳送不接收

我們知道,傳送者一般都會配有相應的接收者。理想情況下,我們希望接收者總能接收完所有傳送的資料,這樣就不會有任何問題。但現實是,一旦接收者發生異常退出,停止繼續接收上游資料,傳送者就會被阻塞。這個情況在 前面說的文章[3] 中有非常細緻的介紹。

示例**:

package main

import "time"

func gen(nums ...int) int)

go func() , nums ...int) 程式設計客棧 done thing, 可能異常中斷接收

if true

}}函式 gen 中通過 select 實現 2 個 channel 的同時處理。當異常發生時,將進入 程式設計客棧釋放,退出時等待了幾秒保證釋放完成。

執行後的輸出如下:

the number of goroutines:  1

現在只有主 goroutine 存在。

接收不傳送

傳送不接收會導致傳送者阻塞,反之,接收不傳送也會導致接收者阻塞。直接看示例**,如下:

package main

func main() ()

var ch chan struct{}

go func() ()

var ch chan int

go func() ()

done := make(chan struct{})

var ch chan int

go func() ()

select ()

var mutex sync.mutex

for i := 0; i < 2; i++ ()

}}執行結果如下:

total: 1

the number of goroutines: 2

這段**通過啟動兩個 go程式設計客棧routine 對 total 進行加法操作,為防止出現資料競爭,對計算部分做了加鎖保護,但並沒有及時的解鎖,導致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 釋放鎖。可以看到,退出時有 2 個 goroutine 存在,出現了洩露,total 的值為 1。

怎麼解決?因為 go 有 defer 的存在,這個問題還是非常容易解決的,只要記得在 lock 的時候,記住 defer unlock 即可。

示例如下:

mutex.lock()

defer mutext.unlock()

其他的鎖與這裡其實都是類似的。

waitgroup

waitgroup 和鎖有所差別,它類似 linux 中的訊號量,可以實現一組 goroutine 操作的等待。使用的時候,如果設定了錯誤的任務數,也可能會導致阻塞,導致洩露發生。

乙個例子,我們在開發乙個後端介面時需要訪問多個資料表,由於資料間沒有依賴關係,我們可以併發訪問,示例如下:

package main

import (

"fmt"

"runtime"

"sync"

"time"

)func handle() ()

go func() ()

go func() ()

wg.wait()

}func main() ()

go handle()

time.sleep(time.second)

}執行結果如下:

the number of goroutines: 2

出現了洩露。再看**,它的開始部分定義了型別為sync.waitgroup的變數 wg,設定併發任務數為 4,但是從例子中可以看出只有 3 個併發任務。故最後的wg.wait()等待退出條件將永遠無法滿足,handle 將會一直阻塞。

怎麼防止這類情況發生?

我個人的建議是,盡量不要一次設定全部任務數,即使數量非常明確的情況。因為在開始多個併發任務之間或許也可能出現被阻斷的情況發生。最好是盡量在任務啟動時通過 wg.add(1) 的方式增加。

示例如下:

...wg.add(1)

go func() ()

wg.add(1)

go func() ()

wg.add(1)

go func() ()

...總結

大概介紹完了我認為的所有可能導致 goroutine 洩露的情況。總結下來,其實無論是死迴圈、channel 阻塞、鎖等待,只要是會造成阻塞的寫法都可能產生洩露。因而,如何防止 goroutine 洩露就變成了如何防止發生阻塞。為進一步防止洩露,有些實現中會加入超時處理,主動釋放處理時間太長的 goroutine。

本文標題: go 防止 goroutine 洩露的方法

本文位址:

Go併發模式之 防止goroutine洩漏

goroutine 有以下幾種方式被終止 1。當他完成了它的工作。2。因為不可恢復的錯誤,它不能繼續工作 3。當他被告知 需要終止工作。我們可以簡單的使用前兩種方法,因為這兩種方法隱含在你的演算法中,但 取消工作 又是怎樣工作的呢?例如 這樣情況 子goroutine 是否該繼續執行可能是以許多其他...

防止 gdi 洩露

gdi使用的幾個注意點 1 create出來的gdi物件,要用deleteobject釋放,create出來的dc,要用deletedc釋放,getdc得出的dc,用releasedc釋放。2 先create後delete,create1,create2,delete2,delete1的順序。3 畫...

Go語言學習 goroutine

簡介 goroutine是go語言中最為nb的設計,也是其魅力所在,goroutine的本質是協程,是實現平行計算的核心。goroutine使用方式非常的簡單,只需使用go關鍵字即可啟動乙個協程,並且它是處於非同步方式執行,你不需要等它執行完成以後在執行以後的 go func 通過go關鍵字啟動乙個...