@ -1,6 +1,6 @@
## 8.5. 併行的循環
本节中, 我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现, 只需要从gopl.io下载 它。
本節中, 我們會探索一些用來在併行時循環迭代的常見併發模型。我們會探究從全尺寸圖片生成一些縮略圖的問題。gopl.io/ch8/thumbnail包提供了ImageFile函數來幫我們拉伸圖片。我們不會説明這個函數的實現, 隻需要從gopl.io下載 它。
```go
gopl.io/ch8/thumbnail
@ -11,7 +11,7 @@ package thumbnail
func ImageFile(infile string) (string, error)
```
下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图 :
下面的程序會循環迭代一些圖片文件名,併爲每一張圖片生成一個縮略圖 :
```go
gopl.io/ch8/thumbnail
@ -25,9 +25,9 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注: embarrassingly parallel, 直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且是最能够享受并发带来的好处,能够随着并行的规模线性地扩 展。
顯然我們處理文件的順序無關緊要,因爲每一個圖片的拉伸操作和其它圖片的處理操作都是彼此獨立的。像這種子問題都是完全彼此獨立的問題被叫做易併行問題(譯註: embarrassingly parallel, 直譯的話更像是尷尬併行)。易併行問題是最容易被實現成併行的一類問題(廢話),併且是最能夠享受併發帶來的好處,能夠隨着併行的規模線性地擴 展。
下面让我们并行地执行这些操作, 从而将文件IO的延迟隐藏掉, 并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误, 之后再进行处 理。
下面讓我們併行地執行這些操作, 從而將文件IO的延遲隱藏掉, 併用上多覈cpu的計算能力來拉伸圖像。我們的第一個併發程序隻是使用了一個go關鍵字。這里我們先忽略掉錯誤, 之後再進行處 理。
```go
// NOTE: incorrect!
@ -38,9 +38,9 @@ func makeThumbnails2(filenames []string) {
}
```
这个版本运行的实在有点太快, 实际上, 由于它比最早的版本使用的时间要短得多, 即使当文件名的slice中只包含有一个元素。这就有点奇怪了, 如果程序没有并发执行的话, 那为什么一个并发的版本还是要快呢? 答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine, 没一个文件名对应一个, 但没有等待它们一直到执行完毕 。
這個版本運行的實在有點太快, 實際上, 由於它比最早的版本使用的時間要短得多, 卽使當文件名的slice中隻包含有一個元素。這就有點奇怪了, 如果程序沒有併發執行的話, 那爲什麽一個併發的版本還是要快呢? 答案其實是makeThumbnails在它還沒有完成工作之前就已經返迴了。它啟動了所有的goroutine, 沒一個文件名對應一個, 但沒有等待它們一直到執行完畢 。
没有什么直接的办法能够等待goroutine完成, 但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓, 使用的方式是向一个共享的channel中发送事件。因为我们已经知道内部的goroutine只有len(filenames), 所以外部的goroutine只需要在返回之前对这些事件计数 。
沒有什麽直接的辦法能夠等待goroutine完成, 但是我們可以改變goroutine里的代碼讓其能夠將完成情況報告給外部的goroutine知曉, 使用的方式是向一個共享的channel中發送事件。因爲我們已經知道內部的goroutine隻有len(filenames), 所以外部的goroutine隻需要在返迴之前對這些事件計數 。
```go
// makeThumbnails3 makes thumbnails of the specified files in parallel.
@ -59,7 +59,7 @@ func makeThumbnails3(filenames []string) {
}
```
注意我们将f的值作为一个显式的变量传给了函数, 而不是在循环的闭包中声 明:
註意我們將f的值作爲一個顯式的變量傳給了函數, 而不是在循環的閉包中聲 明:
```go
@ -71,9 +71,9 @@ for _, f := range filenames {
}
```
回忆一下之前在5.6.1节中, 匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享, 且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时, for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环, 所以当这些goroutine开始读取f的值时, 它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数, 我们能够确保使用的f是当go语句执行时的“当前”那个 f。
迴憶一下之前在5.6.1節中, 匿名函數中的循環變量快照問題。上面這個單獨的變量f是被所有的匿名函數值所共享, 且會被連續的循環迭代所更新的。當新的goroutine開始執行字面函數時, for循環可能已經更新了f併且開始了另一輪的迭代或者(更有可能的)已經結束了整個循環, 所以當這些goroutine開始讀取f的值時, 它們所看到的值已經是slice的最後一個元素了。顯式地添加這個參數, 我們能夠確保使用的f是當go語句執行時的“當前”那個 f。
如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢? 当我们调用thumbnail.ImageFile创建文件失败的时候, 它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误 :
如果我們想要從每一個worker goroutine往主goroutine中返迴值時該怎麽辦呢? 當我們調用thumbnail.ImageFile創建文件失敗的時候, 它會返迴一個錯誤。下一個版本的makeThumbnails會返迴其在做拉伸操作時接收到的第一個錯誤 :
```go
// makeThumbnails4 makes thumbnails for the specified files in parallel.
@ -98,11 +98,11 @@ func makeThumbnails4(filenames []string) error {
}
```
这个程序有一个微秒的bug。当它遇到第一个非nil的error时会直接将error返回到调用方, 使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时, 都会永远地阻塞下去, 并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4), 可能会导致整个程序卡住或者跑出out of memory的错误 。
這個程序有一個微秒的bug。當它遇到第一個非nil的error時會直接將error返迴到調用方, 使得沒有一個goroutine去排空errors channel。這樣剩下的worker goroutine在向這個channel中發送值時, 都會永遠地阻塞下去, 併且永遠都不會退出。這種情況叫做goroutine洩露(§8.4.4), 可能會導致整個程序卡住或者跑出out of memory的錯誤 。
最简单的解决办法就是用一个具有合适大小的buffered channel, 这样这些worker goroutine向channel中发送测向时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine, 当main goroutine返回第一个错误的同时 去排空channel)
最簡單的解決辦法就是用一個具有合適大小的buffered channel, 這樣這些worker goroutine向channel中發送測向時就不會被阻塞。(一個可選的解決辦法是創建一個另外的goroutine, 當main goroutine返迴第一個錯誤的同時 去排空channel)
下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字, 附带生成时的错误 。
下一個版本的makeThumbnails使用了一個buffered channel來返迴生成的圖片文件的名字, 附帶生成時的錯誤 。
```go
// makeThumbnails5 makes thumbnails for the specified files in parallel.
@ -136,9 +136,9 @@ func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
}
```
我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里, 而是通过一个string的channel传过来, 所以我们无法对循环的次数进行预测 。
我們最後一個版本的makeThumbnails返迴了新文件們的大小總計數(bytes)。和前面的版本都不一樣的一點是我們在這個版本里沒有把文件名放在slice里, 而是通過一個string的channel傳過來, 所以我們無法對循環的次數進行預測 。
为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始), 我们需要一个递增的计数器, 在每一个goroutine启动时加一, 在goroutine退出时减一。这需要一种特殊的计数器, 这个计数器需要在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup, 下面的代码就用到了这种 方法:
爲了知道最後一個goroutine什麽時候結束(最後一個結束併不一定是最後一個開始), 我們需要一個遞增的計數器, 在每一個goroutine啟動時加一, 在goroutine退出時減一。這需要一種特殊的計數器, 這個計數器需要在多個goroutine操作時做到安全併且提供提供在其減爲零之前一直等待的一種方法。這種計數類型被稱爲sync.WaitGroup, 下面的代碼就用到了這種 方法:
```go
// makeThumbnails6 makes thumbnails for each file received from the channel.
@ -176,14 +176,14 @@ func makeThumbnails6(filenames <-chan string) int64 {
}
```
注意Add和Done方法的不对策。Add是为计数器加一, 必须在worker goroutine开始之前调用, 而不是在goroutine中; 否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前被调用。并且Add还有一个参数, 但Done却没有任何参数; 其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环, 但又不知道迭代次数时很通常而且很地道的写 法。
註意Add和Done方法的不對策。Add是爲計數器加一, 必鬚在worker goroutine開始之前調用, 而不是在goroutine中; 否則的話我們沒辦法確定Add是在"closer" goroutine調用Wait之前被調用。併且Add還有一個參數, 但Done卻沒有任何參數; 其實它和Add(-1)是等價的。我們使用defer來確保計數器卽使是在出錯的情況下依然能夠正確地被減掉。上面的程序代碼結構是當我們使用併發循環, 但又不知道迭代次數時很通常而且很地道的寫 法。
sizes channel携带了每一个文件的大小到main goroutine, 在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine, 并让其等待worker们在关闭掉sizes channel之前退出的。两步操作: wait和close, 必须是基于sizes的循环的并发。考虑一下另一种方案: 如果等待操作被放在了main goroutine中, 在循环之前, 这样的话就永远都不会结束了, 如果在循环之后, 那么又变成了不可达的部分, 因为没有任何东西去关闭这个channel, 这个循环就永远都不会终 止。
sizes channel攜帶了每一個文件的大小到main goroutine, 在main goroutine中使用了range loop來計算總和。觀察一下我們是怎樣創建一個closer goroutine, 併讓其等待worker們在關閉掉sizes channel之前退出的。兩步操作: wait和close, 必鬚是基於sizes的循環的併發。考慮一下另一種方案: 如果等待操作被放在了main goroutine中, 在循環之前, 這樣的話就永遠都不會結束了, 如果在循環之後, 那麽又變成了不可達的部分, 因爲沒有任何東西去關閉這個channel, 這個循環就永遠都不會終 止。
图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep, 粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环, 等待worker发送值或者closer来关闭 channel的。
圖8.5 表明了makethumbnails6函數中事件的序列。縱列表示goroutine。窄線段代表sleep, 粗線段代表活動。斜線箭頭代表用來同步兩個goroutine的事件。時間向下流動。註意main goroutine是如何大部分的時間被喚醒執行其range循環, 等待worker發送值或者closer來關閉 channel的。
![](../images/ch8-05.png)
练习8.4: 修改reverb2服务器, 在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时, 关闭TCP连接的写入, 像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成, 即使是在标准输入流已经关闭的情况 下。
練習8.4: 脩改reverb2服務器, 在每一個連接中使用sync.WaitGroup來計數活躍的echo goroutine。當計數減爲零時, 關閉TCP連接的寫入, 像練習8.3中一樣。驗證一下你的脩改版netcat3客戶端會一直等待所有的併發“喊叫”完成, 卽使是在標準輸入流已經關閉的情況 下。
练习8.5: 使用一个已有的CPU绑定的顺序程序, 比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序, 并将他们的主循环改为并发形式, 使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进? 使用多少个goroutine是最合适 的呢?
練習8.5: 使用一個已有的CPU綁定的順序程序, 比如在3.3節中我們寫的Mandelbrot程序或者3.2節中的3-D surface計算程序, 併將他們的主循環改爲併發形式, 使用channel來進行通信。在多覈計算機上這個程序得到了多少速度上的改進? 使用多少個goroutine是最合適 的呢?