goroutines占用資源較少且易於創建。執行時將多個 goroutine復用到任意數量的作業系統執行緒,以便我們不必擔心抽象層級。但是他們會花費成本資源,並且goroutine不會被runtime GC,所以無論記憶體占用多少,我們都應該確保他們被清理乾淨
goroutines代表可能平行(parallel)或不平行運行的工作單位。該goroutine有幾條路徑終止:
1.當它完成任務。
2.當它遇到不可恢復的錯誤無法繼續它的任務。
3.當它被告知停止當前任務。
前兩條我們已經知曉,可以通過算法實現。但如何取消當前任務?
由於網絡效應,這最重要的一點是:如果你已經開始了一個goroutine,那麼它很可能以某種有組織的方式與其他幾個goroutines合作。我們甚至可以把這種相互連接表現為一張圖表,這時該goroutine能否停下來還取決於處在互動的其他 goroutines。我們將在下一章中繼續關注大規模並發產生的相互依賴關係,但現在讓我們考慮如何確保單個goroutine得到清理。讓我們從一個簡單的goroutine洩漏開始:
package main
import (
"fmt"
)
func main() {
doWork := func(strings <-chan string) <-chan interface{} {
completed := make(chan interface{})
go func() {
defer fmt.Println("doWork exited.")
defer close(completed)
for s := range strings {
// Do something interesting
fmt.Println(s)
}
}()
return completed
}
doWork(nil)
// Perhaps more work is done here
fmt.Println("Done.")
}
這行代碼調用了doWork函數,但傳遞了nil作為strings通道。這意味著,內部的goroutine的迴圈將不會接收到任何數據,因為對一個nil通道的接收操作永遠都不會返回。
當您運行這個程序時,它將僅僅輸出 "Done."。這是因為doWork內部的goroutine無法從nil通道接收到任何數據,且strings通道從未被關閉(因為它是nil),所以"doWork exited."也不會被輸出。
strings通道永遠無法讀取到內容(因為它是nil),而且包含 doWork的goroutine將在這個過程的整個生命周期中保留在內存中(如果我們在doWork和主goutoutine中加入了goroutine,我們甚至會死鎖)。
在這個例子中,整個進程的生命周期很短,但是在一個真正的程序中,goroutines可以很容易地在一個長期生命的程序開始時啟動,導致內存利用率下降。
解決這種情況的方法是建立一個信號,按照慣例,這個信號通常是一個名為done的只讀通道。父例程將該通道傳遞給子例程,然後在想要取消子例程時關閉該通道。
package main
import (
"fmt"
"time"
)
func main() {
// doWork函數現在接受一個新的參數:done通道。當這個通道被關閉時,內部的goroutine應當停止其工作並退出。
doWork := func(
done <-chan interface{},
strings <-chan string,
) <-chan interface{} { // <1>
terminated := make(chan interface{})
go func() {
defer fmt.Println("doWork exited.")
defer close(terminated)
// 在goroutine中,select語句用於同時等待多個通道操作。如果strings通道接收到新的字符串,它將處理和輸出該字符串。如果done通道被關閉,那麼goroutine將結束。
for {
select {
case s := <-strings:
// Do something interesting
fmt.Println(s)
case <-done: // <2>
return
}
}
}()
return terminated
}
done := make(chan interface{})
terminated := doWork(done, nil)
// 在主函式main中,啟動了另一個goroutine來在1秒後關閉done通道。這表示doWork中的goroutine將在大約1秒後收到終止信號並退出。
go func() { // <3>
// Cancel the operation after 1 second.
time.Sleep(1 * time.Second)
fmt.Println("Canceling doWork goroutine...")
close(done)
}()
// 主函式main會阻塞,直到doWork中的goroutine完全終止。這確保了在程序的main函式結束前,所有背景工作都已經完成。
<-terminated // <4>
fmt.Println("Done.")
}
這個改善的版本提供了一種方法來優雅地終止和清理goroutines,防止了潛在的goroutine泄露。它通過done通道提供了一個外部信號來告訴doWork中的goroutine何時該停止工作。這種模式在Go的併發設計中是很常見的,因為它允許更好地控制和管理goroutines。