正如我們在“Goroutines”一節中介紹的那樣,goroutines占用資源較少且易于創建。運行時將多個goroutine復用到任意數量的操作系統線程,以便我們不必擔心抽象級別。但是他們會花費成本資源,并且goroutine不會被運行時垃圾收集,所以無論內存占用多少,我們都不想讓他們對我們的進程撒謊。 那么我們如何去確保他們被清理干凈?
讓我們從頭開始,一步一步思考:為什么會有一個goroutine? 在第二章中,我們確定,goroutines代表可能并行或不可以并行運行的工作單元。 該goroutine有幾條路徑終止:
* 當它完成任務。
* 當它遇到不可恢復的錯誤無法繼續它的任務。
* 當它被告知停止當前任務。
前兩條我們已經知曉,可以通過算法實現。但如何取消當前任務?由于網絡效應,這最重要的一點是:如果你已經開始了一個goroutine,那么它很可能以某種有組織的方式與其他幾個goroutines合作。我們甚至可以把這種相互連接表現為一張圖表,這時該goroutine能否停下來還取決于處在交互的其他goroutines。我們將在下一章中繼續關注大規模并發產生的相互依賴關系,但現在讓我們考慮如何確保保證單個goroutine得到清理。 讓我們從一個簡單的goroutine泄漏開始:
```
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 {
fmt.Println(s)
}
}()
return completed
}
doWork(nil)
// 這里還有其他任務執行
fmt.Println("Done.")
```
我們看到doWork被傳遞了一個nil通道。所以strings通道永遠無法讀取到其承載的內容,而且包含doWork的goroutine將在這個過程的整個生命周期中保留在內存中(如果我們在doWork和主goutoutine中加入了goroutine,我們甚至會死鎖)。
在這個例子中,整個進程的生命周期很短,但是在一個真正的程序中,goroutines可以很容易地在一個長期生命的程序開始時啟動,導致內存利用率下降。
解決這種情況的方法是建立一個信號,按照慣例,這個信號通常是一個名為done的只讀通道。父例程將該通道傳遞給子例程,然后在想要取消子例程時關閉該通道。 這是一個例子:
```
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)
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)
go func() { //3
// Cancel the operation after 1 second.
time.Sleep(1 * time.Second)
fmt.Println("Canceling doWork goroutine...")
close(done)
}()
<-terminated //4
fmt.Println("Done.")
```
1. 這里我們傳遞done通道給doWork函數。作為慣例,這個通道被作為首個參數。
2. 這里我們看到使用了for-select的使用模式之一。我們的目的是檢查done通道有沒有發出信號。如果有的話,我們退出當前goroutine。
3. 在這里我們創建另一個goroutine,一秒后就會取消doWork中產生的goroutine。
4. 這是我們在main goroutine中調用doWork函數返回結果的地方。
這會輸出:
```
Canceling doWork goroutine...
doWork exited.
Done.
```
你可以看到盡管向doWork傳遞了nil給strings通道,我們的goroutine依然正常運行至結束。與之前的例子不同,本例中我們把兩個goroutine連接在一起之前,我們建立了第三個goroutine以取消doWork中的goroutine,并成功消除了泄漏問題。
前面的例子很好地處理了在通道上接收goroutine的情況,但是如果我們正在處理相反的情況:在嘗試向通道寫入值時阻塞goroutine會怎樣?
```
newRandStream := func() <-chan int {
randStream := make(chan int)
go func() {
defer fmt.Println("newRandStream closure exited.") // 1
defer close(randStream)
for {
randStream <- rand.Int()
}
}()
return randStream
}
randStream := newRandStream()
fmt.Println("3 random ints:")
for i := 1; i <= 3; i++ {
fmt.Printf("%d: %d\n", i, <-randStream)
}
```
1. 當goroutine成功執行時我們打印一行消息。
這會輸出:
```
3 random ints:
1: 5577006791947779410
2: 8674665223082153551
3: 6129484611666145821
```
你可以看到注釋1所在的打印語句并未執行。在循環的第三次迭代之后,我們的goroutine塊試圖將下一個隨機整數發送到不再被讀取的通道。我們無法告知它停下來,解決方案是為生產者提供一條通知它退出的通道:
```
newRandStream := func(done <-chan interface{}) <-chan int {
randStream := make(chan int)
go func() {
defer fmt.Println("newRandStream closure exited.")
defer close(randStream)
for {
select {
case randStream <- rand.Int():
case <-done:
return
}
}
}()
return randStream
}
done := make(chan interface{})
randStream := newRandStream(done)
fmt.Println("3 random ints:")
for i := 1; i <= 3; i++ {
fmt.Printf("%d: %d\n", i, <-randStream)
}
close(done)
//模擬正在進行的工作
time.Sleep(1 * time.Second)
```
這會輸出:
```
3 random ints:
1: 5577006791947779410
2: 8674665223082153551
3: 6129484611666145821
newRandStream closure exited.
```
我們現在看到該goroutine被妥善清理。
現在我們知道如何確保goroutine不泄漏,我們可以制定一個約定:如果goroutine負責創建goroutine,它也負責確保它可以停止goroutine。
這個約定有助于確保程序在組合和擴展時可用。我們將在“管道”和“context包”中重新討論這種技術和規則。我們該如何確保goroutine能夠被停止根據goroutine的類型和用途而有所不同,但是它們 所有這些都是建立在傳遞done通道基礎上的。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度