@@ -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的。

练习 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是最合適 的呢?