常見的 Goroutine 泄露,你應(yīng)該避免
Go 語言編寫代碼的最大優(yōu)點(diǎn)之一是能夠在輕量級線程,即 Goroutines 中并發(fā)運(yùn)行你的代碼。
然而,擁有強(qiáng)大的能力也伴隨著巨大的責(zé)任。
盡管 Goroutines 非常方便,但如果不小心處理,它們很容易引入難以追蹤的錯(cuò)誤。
Goroutine 泄露就是其中之一。它在背景中悄悄增長,可能最終在你不知情的情況下使你的應(yīng)用程序崩潰。
因此,本文主要介紹 Goroutine 泄露是什么,以及你如何防止泄露發(fā)生。
我們來看看吧!
什么是 Goroutine 泄露?
當(dāng)創(chuàng)建一個(gè)新的 Goroutine 時(shí),計(jì)算機(jī)在堆中分配內(nèi)存,并在執(zhí)行完成后釋放它們。
Goroutine 泄露是一種內(nèi)存泄露,當(dāng) Goroutine 沒有終止并在應(yīng)用程序的生命周期中被留在后臺時(shí)就會發(fā)生。
讓我們來看一個(gè)簡單的例子。
func goroutineLeak(ch chan int) {
data := <- ch
fmt.Println(data)
}
func handler() {
ch := make(chan int)
go goroutineLeak(ch)
return
}
隨著處理器的返回,Goroutine 繼續(xù)在后臺活動,阻塞并等待數(shù)據(jù)通過通道發(fā)送 —— 這永遠(yuǎn)不會發(fā)生。
因此,產(chǎn)生了一個(gè) Goroutine 泄露。
在本文中,我將引導(dǎo)你了解兩種常見的模式,這些模式很容易導(dǎo)致 Goroutine 泄漏:
- 遺忘的發(fā)送者
- 被遺棄的接收者
讓我們深入研究!
遺忘的發(fā)送者
遺忘的發(fā)送者發(fā)生在發(fā)送者被阻塞,因?yàn)闆]有接收者在通道的另一側(cè)等待接收數(shù)據(jù)的情況。
func forgottenSender(ch chan int) {
data := 3
// This is blocked as no one is receiving the data
ch <- data
}
func handler () {
ch := make(chan int)
go forgottenSender(ch)
return
}
雖然它起初看起來很簡單,但在以下兩種情境中很容易被忽視。
不當(dāng)使用 Context
func forgottenSender(ch chan int) {
data := networkCall()
ch <- data
}
func handler() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
ch := make(chan int)
go forgottenSender(ch)
select {
case data := <- ch: {
fmt.Printf("Received data! %s", data)
return nil
}
case <- ctx.Done(): {
return errors.New("Timeout! Process cancelled. Returning")
}
}
}
在上面的例子中,我們模擬了一個(gè)標(biāo)準(zhǔn)的網(wǎng)絡(luò)服務(wù)處理程序。
我們定義了一個(gè)上下文,它在10ms后發(fā)出超時(shí),隨后是一個(gè)異步進(jìn)行網(wǎng)絡(luò)調(diào)用的Goroutine。
select語句等待多個(gè)通道操作。它會阻塞,直到其其中一個(gè)情況可以運(yùn)行并執(zhí)行該情況。
如果網(wǎng)絡(luò)調(diào)用完成之前超時(shí)到達(dá),case <- ctx.Done() 將會執(zhí)行,處理程序?qū)⒎祷匾粋€(gè)錯(cuò)誤。
當(dāng)處理程序返回時(shí),不再有任何接收者等待接收數(shù)據(jù)。forgottenSender將被阻塞,等待有人接收數(shù)據(jù),但這永遠(yuǎn)不會發(fā)生!
這就是Goroutine泄露的地方。
錯(cuò)誤檢查后的接收者位置
這是另一個(gè)典型的情況。
func forgottenSender(ch chan int) {
data := networkCall()
ch <- data
}
func handler() error {
ch := make(chan int)
go forgottenSender(ch)
err := continueToValidateOtherData()
if err != nil {
return errors.New("Data is invalid! Returning.")
}
data := <- ch
return nil
}
在上面的例子中,我們定義了一個(gè)處理程序并生成一個(gè)新的Goroutine來異步進(jìn)行網(wǎng)絡(luò)調(diào)用。
在等待調(diào)用返回的過程中,我們繼續(xù)其他的驗(yàn)證邏輯。
如你所見,當(dāng)continueToValidateOtherData返回一個(gè)錯(cuò)誤導(dǎo)致處理程序返回時(shí),泄露就發(fā)生了。
沒有人等待接收數(shù)據(jù),forgottenSender將永遠(yuǎn)被阻塞!
解決方案:忘記的發(fā)送者
使用一個(gè)緩沖通道。
如果你回想一下,忘記的發(fā)送者發(fā)生是因?yàn)榱硪欢藳]有接收者。阻塞問題的罪魁禍?zhǔn)资且粋€(gè)無緩沖的通道!
一個(gè)無緩沖的通道是在消息發(fā)出時(shí)立即需要一個(gè)接收者的,否則發(fā)送者會被阻塞。它是在沒有為通道分配容量的情況下聲明的。
func forgottenSender(ch chan int) {
data := 3
// This will NOT block
ch <- data
}
func handler() {
// Declare a BUFFERED channel
ch := make(chan int, 1)
go forgottenSender(ch)
return
}
通過為通道添加特定的容量,在這種情況下為1,我們可以減少所有提到的問題。
發(fā)送者可以在不需要接收者的情況下將數(shù)據(jù)注入通道。
被遺棄的接收者
正如其名字所暗示的,被遺棄的接收者是完全相反的情況。
當(dāng)一個(gè)接收者被阻塞,因?yàn)榱硪贿厸]有發(fā)送者發(fā)送數(shù)據(jù)時(shí),它就會發(fā)生。
func abandonedReceiver(ch chan int) {
// This will be blocked
data := <- ch
fmt.Println(data)
}
func handler() {
ch := make(chan int)
go abandonedReceiver(ch)
return
}
第3行一直被阻塞,因?yàn)闆]有發(fā)送者發(fā)送數(shù)據(jù)。
讓我們再次了解兩個(gè)常見的場景,這些場景經(jīng)常被忽視。
發(fā)送者未關(guān)閉的通道
func abandonedWorker(ch chan string) {
for data := range ch {
processData(data)
}
fmt.Println("Worker is done, shutting down")
}
func handler(inputData []string) {
ch := make(chan string, len(inputData))
for _, data := range inputData {
ch <- data
}
go abandonedWorker(ch)
return
}
在上面的例子中,處理程序接收一個(gè)字符串切片,創(chuàng)建一個(gè)通道并將數(shù)據(jù)插入到通道中。
處理程序然后通過Goroutine啟動一個(gè)工作程序。工作程序預(yù)計(jì)會處理數(shù)據(jù),并且一旦處理完通道中的所有數(shù)據(jù),就會終止。
然而,即使消耗并處理了所有的數(shù)據(jù),工作程序也永遠(yuǎn)不會到達(dá)“第6行”!
盡管通道是空的,但它沒有被關(guān)閉!工作程序繼續(xù)認(rèn)為未來可能會有傳入的數(shù)據(jù)。因此,它坐下來并永遠(yuǎn)等待。
這是Goroutine再次泄漏的地方。
在錯(cuò)誤檢查之后放置發(fā)送者
這與我們之前的一些示例非常相似。
func abandonedWorker(ch chan []int) {
data := <- ch
fmt.Println(data)
}
func handler() error {
ch := make(chan []int)
go abandonedWorker(ch)
records, err := getFromDB()
if err != nil {
return errors.New("Database error. Returning")
}
ch <- records
return nil
}
在上面的例子中,處理程序首先啟動一個(gè)Goroutine工作程序來處理和消費(fèi)一些數(shù)據(jù)。
然后,處理程序從數(shù)據(jù)庫中查詢記錄,然后將記錄注入通道供工作程序使用。
如果數(shù)據(jù)庫出現(xiàn)錯(cuò)誤,處理程序?qū)⒘⒓捶祷?。通道將不再有任何發(fā)送者傳入數(shù)據(jù)。
因此,工作程序被遺棄。
解決方案:被遺棄的接收者
在這兩種情況下,接收者都被留下,因?yàn)樗麄儭罢J(rèn)為”通道將有傳入的數(shù)據(jù)。因此,它們阻塞并永遠(yuǎn)等待。
解決方案是一個(gè)簡單的單行代碼。
defer close(ch)
當(dāng)你啟動一個(gè)新的通道時(shí),最好的做法是推遲關(guān)閉通道。
這確保在數(shù)據(jù)發(fā)送完成或函數(shù)退出時(shí)關(guān)閉通道。
接收者可以判斷一個(gè)通道是否已關(guān)閉,并相應(yīng)地終止。
func abandonedReceiver(ch chan int) {
// This will NOT be blocked FOREVER
data := <- ch
fmt.Println(data)
}
func handler() {
ch := make(chan int)
// Defer the CLOSING of channel
defer close(ch)
go abandonedReceiver(ch)
return
}
結(jié)論
關(guān)于 Goroutine 泄漏就是這么多了!
盡管它不像其他 Goroutine 錯(cuò)誤那么強(qiáng)大,但這種泄漏仍然會大大耗盡應(yīng)用程序的內(nèi)存使用。
記住,擁有強(qiáng)大的力量也伴隨著巨大的責(zé)任。
保護(hù)我們的應(yīng)用程序免受錯(cuò)誤的責(zé)任在于你我——開發(fā)人員!