常見的 Goroutine 泄露,你應該避免
Go 語言編寫代碼的最大優(yōu)點之一是能夠在輕量級線程,即 Goroutines 中并發(fā)運行你的代碼。
然而,擁有強大的能力也伴隨著巨大的責任。
盡管 Goroutines 非常方便,但如果不小心處理,它們很容易引入難以追蹤的錯誤。
Goroutine 泄露就是其中之一。它在背景中悄悄增長,可能最終在你不知情的情況下使你的應用程序崩潰。
因此,本文主要介紹 Goroutine 泄露是什么,以及你如何防止泄露發(fā)生。
我們來看看吧!
什么是 Goroutine 泄露?
當創(chuàng)建一個新的 Goroutine 時,計算機在堆中分配內存,并在執(zhí)行完成后釋放它們。
Goroutine 泄露是一種內存泄露,當 Goroutine 沒有終止并在應用程序的生命周期中被留在后臺時就會發(fā)生。
讓我們來看一個簡單的例子。
func goroutineLeak(ch chan int) {
data := <- ch
fmt.Println(data)
}
func handler() {
ch := make(chan int)
go goroutineLeak(ch)
return
}
隨著處理器的返回,Goroutine 繼續(xù)在后臺活動,阻塞并等待數據通過通道發(fā)送 —— 這永遠不會發(fā)生。
因此,產生了一個 Goroutine 泄露。
在本文中,我將引導你了解兩種常見的模式,這些模式很容易導致 Goroutine 泄漏:
- 遺忘的發(fā)送者
- 被遺棄的接收者
讓我們深入研究!
遺忘的發(fā)送者
遺忘的發(fā)送者發(fā)生在發(fā)送者被阻塞,因為沒有接收者在通道的另一側等待接收數據的情況。
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
}
雖然它起初看起來很簡單,但在以下兩種情境中很容易被忽視。
不當使用 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")
}
}
}
在上面的例子中,我們模擬了一個標準的網絡服務處理程序。
我們定義了一個上下文,它在10ms后發(fā)出超時,隨后是一個異步進行網絡調用的Goroutine。
select語句等待多個通道操作。它會阻塞,直到其其中一個情況可以運行并執(zhí)行該情況。
如果網絡調用完成之前超時到達,case <- ctx.Done() 將會執(zhí)行,處理程序將返回一個錯誤。
當處理程序返回時,不再有任何接收者等待接收數據。forgottenSender將被阻塞,等待有人接收數據,但這永遠不會發(fā)生!
這就是Goroutine泄露的地方。
錯誤檢查后的接收者位置
這是另一個典型的情況。
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
}
在上面的例子中,我們定義了一個處理程序并生成一個新的Goroutine來異步進行網絡調用。
在等待調用返回的過程中,我們繼續(xù)其他的驗證邏輯。
如你所見,當continueToValidateOtherData返回一個錯誤導致處理程序返回時,泄露就發(fā)生了。
沒有人等待接收數據,forgottenSender將永遠被阻塞!
解決方案:忘記的發(fā)送者
使用一個緩沖通道。
如果你回想一下,忘記的發(fā)送者發(fā)生是因為另一端沒有接收者。阻塞問題的罪魁禍首是一個無緩沖的通道!
一個無緩沖的通道是在消息發(fā)出時立即需要一個接收者的,否則發(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ā)送者可以在不需要接收者的情況下將數據注入通道。
被遺棄的接收者
正如其名字所暗示的,被遺棄的接收者是完全相反的情況。
當一個接收者被阻塞,因為另一邊沒有發(fā)送者發(fā)送數據時,它就會發(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行一直被阻塞,因為沒有發(fā)送者發(fā)送數據。
讓我們再次了解兩個常見的場景,這些場景經常被忽視。
發(fā)送者未關閉的通道
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
}
在上面的例子中,處理程序接收一個字符串切片,創(chuàng)建一個通道并將數據插入到通道中。
處理程序然后通過Goroutine啟動一個工作程序。工作程序預計會處理數據,并且一旦處理完通道中的所有數據,就會終止。
然而,即使消耗并處理了所有的數據,工作程序也永遠不會到達“第6行”!
盡管通道是空的,但它沒有被關閉!工作程序繼續(xù)認為未來可能會有傳入的數據。因此,它坐下來并永遠等待。
這是Goroutine再次泄漏的地方。
在錯誤檢查之后放置發(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
}
在上面的例子中,處理程序首先啟動一個Goroutine工作程序來處理和消費一些數據。
然后,處理程序從數據庫中查詢記錄,然后將記錄注入通道供工作程序使用。
如果數據庫出現(xiàn)錯誤,處理程序將立即返回。通道將不再有任何發(fā)送者傳入數據。
因此,工作程序被遺棄。
解決方案:被遺棄的接收者
在這兩種情況下,接收者都被留下,因為他們“認為”通道將有傳入的數據。因此,它們阻塞并永遠等待。
解決方案是一個簡單的單行代碼。
defer close(ch)
當你啟動一個新的通道時,最好的做法是推遲關閉通道。
這確保在數據發(fā)送完成或函數退出時關閉通道。
接收者可以判斷一個通道是否已關閉,并相應地終止。
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
}
結論
關于 Goroutine 泄漏就是這么多了!
盡管它不像其他 Goroutine 錯誤那么強大,但這種泄漏仍然會大大耗盡應用程序的內存使用。
記住,擁有強大的力量也伴隨著巨大的責任。
保護我們的應用程序免受錯誤的責任在于你我——開發(fā)人員!