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