mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
make loop
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
## 8.1. Goroutines
|
||||
|
||||
在Go語言中,每一個並發的執行單元叫作一個goroutine。設想這裡有一個程序有兩個函數,一個函數做一些計算,另一個輸齣一些結果,假設兩個函數沒有相互之間的調用關繫。一個綫性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在同一時間。我們馬上就會看到這樣的一個程序。
|
||||
在Go語言中,每一個併發的執行單元叫作一個goroutine。設想這里有一個程序有兩個函數,一個函數做一些計算,另一個輸齣一些結果,假設兩個函數沒有相互之間的調用關繫。一個線性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在同一時間。我們馬上就會看到這樣的一個程序。
|
||||
|
||||
如果你使用過操作繫統或者其它語言提供的綫程,那麽你可以簡單地把goroutine類比作一個綫程,這樣你就可以寫齣一些正確的程序了。goroutine和綫程的本質區別會在9.8節中講。
|
||||
如果你使用過操作繫統或者其它語言提供的線程,那麽你可以簡單地把goroutine類比作一個線程,這樣你就可以寫齣一些正確的程序了。goroutine和線程的本質區别會在9.8節中講。
|
||||
|
||||
當一個程序啟動時,其主函數卽在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。在語法上,go語句是一個普通的函數或方法調用前加上關鍵字go。go語句會使其語句中的函數在一個新創建的goroutine中運行。而go語句本身會迅速地完成。
|
||||
|
||||
@@ -11,7 +11,7 @@ f() // call f(); wait for it to return
|
||||
go f() // create a new goroutine that calls f(); don't wait
|
||||
```
|
||||
|
||||
在下面的例子中,main goroutine會計算第45個菲波那契數。由於計算函數使用了效率非常低的遞歸,所以會運行相當可觀的一段時間,在這期間我們想要讓用戶看到一個可見的標識來表明程序依然在正常運行,所以顯示一個動畫的小圖標:
|
||||
在下面的例子中,main goroutine會計算第45個菲波那契數。由於計算函數使用了效率非常低的遞歸,所以會運行相當可觀的一段時間,在這期間我們想要讓用戶看到一個可見的標識來表明程序依然在正常運行,所以顯示一個動畵的小圖標:
|
||||
|
||||
```go
|
||||
gopl.io/ch8/spinner
|
||||
@@ -40,10 +40,10 @@ func fib(x int) int {
|
||||
|
||||
```
|
||||
|
||||
動畫顯示了幾秒之後,fib(45)的調用成功地返迴,並且打印結果:
|
||||
動畵顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果:
|
||||
Fibonacci(45) = 1134903170
|
||||
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退齣。除了從主函數退齣或者直接退齣程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,並讓其自己結束執行。
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退齣。除了從主函數退齣或者直接退齣程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,併讓其自己結束執行。
|
||||
|
||||
註意這裡的兩個獨立的單元是如何進行組合的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會並發地執行。
|
||||
註意這里的兩個獨立的單元是如何進行組合的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會併發地執行。
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 8.2. 示例: 並發的Clock服務
|
||||
## 8.2. 示例: 併發的Clock服務
|
||||
|
||||
網絡編程是並發大顯身手的一個領域,由於服務器是最典型的需要同時處理很多連接的程序,這些連接一般來自遠彼此獨立的客戶端。在本小節中,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,無論兩者間通信是使用TCP,UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包裡的方法,也算是net包的一部分。
|
||||
網絡編程是併發大顯身手的一個領域,由於服務器是最典型的需要同時處理很多連接的程序,這些連接一般來自遠彼此獨立的客戶端。在本小節中,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,無論兩者間通信是使用TCP,UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包里的方法,也算是net包的一部分。
|
||||
|
||||
我們的第一個例子是一個順序執行的時鍾服務器,它會每隔一秒鍾將當前時間寫到客戶端:
|
||||
我們的第一個例子是一個順序執行的時鐘服務器,它會每隔一秒鐘將當前時間寫到客戶端:
|
||||
```go
|
||||
gopl.io/ch8/clock1
|
||||
// Clock1 is a TCP server that periodically writes the time.
|
||||
@@ -44,13 +44,13 @@ func handleConn(c net.Conn) {
|
||||
|
||||
```
|
||||
|
||||
Listen函數創建了一個net.Listener的對象,這個對象會監聽一個網絡端口上到來的連接,在這個例子裡我們用的是TCP的localhost:8000端口。listener對象的Accept方法會直接阻塞,直到一個新的連接被創建,然後會返迴一個net.Conn對象來表示這個連接。
|
||||
Listen函數創建了一個net.Listener的對象,這個對象會監聽一個網絡端口上到來的連接,在這個例子里我們用的是TCP的localhost:8000端口。listener對象的Accept方法會直接阻塞,直到一個新的連接被創建,然後會返迴一個net.Conn對象來表示這個連接。
|
||||
|
||||
handleConn函數會處理一個完整的客戶端連接。在一個for死循環中,將當前的時候用time.Now()函數得到,然後寫到客戶端。由於net.Conn實現了io.Writer接口,我們可以直接向其寫入內容。這個死循環會一直執行,直到寫入失敗。最可能的原因是客戶端主動斷開連接。這種情況下handleConn函數會用defer調用關閉服務器側的連接,然後返迴到主函數,繼續等待下一個連接請求。
|
||||
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組合前面這個模闆;齣現在模闆中的部分會作爲參考來對時間格式進行輸齣。在上面的例子中我們隻用到了小時、分鍾和秒。time包裡定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆向操作time.Parse時,也會用到同樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這裡可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組合前面這個模闆;齣現在模闆中的部分會作爲參考來對時間格式進行輸齣。在上面的例子中我們隻用到了小時、分鐘和秒。time包里定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆向操作time.Parse時,也會用到同樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這里可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
|
||||
爲了連接例子裡的服務器,我們需要一個客戶端程序,比如netcat這個工具(nc命令),這個工具可以用來執行網絡連接操作。
|
||||
爲了連接例子里的服務器,我們需要一個客戶端程序,比如netcat這個工具(nc命令),這個工具可以用來執行網絡連接操作。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/clock1
|
||||
@@ -92,7 +92,7 @@ func mustCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
}
|
||||
```
|
||||
這個程序會從連接中讀取數據,並將讀到的內容寫到標準輸齣中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這裡可以開兩個終端窗口,下面左邊的是其中的一個的輸齣,右邊的是另一個的輸齣:
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸齣中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這里可以開兩個終端窗口,下面左邊的是其中的一個的輸齣,右邊的是另一個的輸齣:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/netcat1
|
||||
@@ -110,7 +110,7 @@ $ killall clock1
|
||||
|
||||
killall命令是一個Unix命令行工具,可以用給定的進程名來殺掉所有名字匹配的進程。
|
||||
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端纔能繼續向後執行;因爲我們這裡的服務器程序同一時間隻能處理一個客戶端連接。我們這裡對服務端程序做一點小改動,使其支持並發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端纔能繼續向後執行;因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動,使其支持併發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/clock2
|
||||
@@ -144,7 +144,7 @@ $ ./netcat1
|
||||
$ killall clock2
|
||||
```
|
||||
|
||||
練習8.1: 脩改clock2來支持傳入參數作爲端口號,然後寫一個clockwall的程序,這個程序可以同時與多個clock服務器通信,從多服務器中讀取時間,並且在一個表格中一次顯示所有服務傳迴的結果,類似於你在某些辦公室裡看到的時鍾牆。如果你有地理學上分佈式的服務器可以用的話,讓這些服務器跑在不同的機器上面;或者在同一檯機器上跑多個不同的實例,這些實例監聽不同的端口,假裝自己在不同的時區。像下面這樣:
|
||||
練習8.1: 脩改clock2來支持傳入參數作爲端口號,然後寫一個clockwall的程序,這個程序可以同時與多個clock服務器通信,從多服務器中讀取時間,併且在一個表格中一次顯示所有服務傳迴的結果,類似於你在某些辦公室里看到的時鐘牆。如果你有地理學上分布式的服務器可以用的話,讓這些服務器跑在不同的機器上面;或者在同一颱機器上跑多個不同的實例,這些實例監聽不同的端口,假裝自己在不同的時區。像下面這樣:
|
||||
|
||||
```
|
||||
$ TZ=US/Eastern ./clock2 -port 8010 &
|
||||
@@ -153,4 +153,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
|
||||
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
|
||||
```
|
||||
|
||||
練習8.2: 實現一個並發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列齣目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
練習8.2: 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列齣目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
## 8.3. 示例: 並發的Echo服務
|
||||
## 8.3. 示例: 併發的Echo服務
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
## 8.5. 並行的循環
|
||||
## 8.5. 併行的循環
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.6. 示例: 並發的Web爬蟲
|
||||
## 8.6. 示例: 併發的Web爬蟲
|
||||
|
||||
在5.6節中,我們做了一個簡單的web爬蟲,用bfs(廣度優先)算法來抓取整個網站。在本節中,我們會讓這個這個爬蟲並行化,這樣每一個彼此獨立的抓取命令可以並行進行IO,最大化利用網絡資源。crawl函數和gopl.io/ch5/findlinks3中的是一樣的。
|
||||
在5.6節中,我們做了一個簡單的web爬蟲,用bfs(廣度優先)算法來抓取整個網站。在本節中,我們會讓這個這個爬蟲併行化,這樣每一個彼此獨立的抓取命令可以併行進行IO,最大化利用網絡資源。crawl函數和gopl.io/ch5/findlinks3中的是一樣的。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/crawl1
|
||||
@@ -14,7 +14,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
主函數和5.6節中的breadthFirst(深度優先)類似。像之前一樣,一個worklist是一個記録了需要處理的元素的隊列,每一個元素都是一個需要抓取的URL列表,不過這一次我們用channel代替slice來做這個隊列。每一個對crawl的調用都會在他們自己的goroutine中進行並且會把他們抓到的鏈接發送迴worklist。
|
||||
主函數和5.6節中的breadthFirst(深度優先)類似。像之前一樣,一個worklist是一個記録了需要處理的元素的隊列,每一個元素都是一個需要抓取的URL列表,不過這一次我們用channel代替slice來做這個隊列。每一個對crawl的調用都會在他們自己的goroutine中進行併且會把他們抓到的鏈接發送迴worklist。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -38,9 +38,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
註意這裡的crawl所在的goroutine會將link作爲一個顯式的參數傳入,來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這裡將命令行參數傳入worklist也是在一個另外的goroutine中進行的,這是爲了避免在main goroutine和crawler goroutine中同時向另一個goroutine通過channel發送內容時發生死鎖(因爲另一邊的接收操作還沒有準備好)。當然,這裡我們也可以用buffered channel來解決問題,這裡不再贅述。
|
||||
註意這里的crawl所在的goroutine會將link作爲一個顯式的參數傳入,來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這里將命令行參數傳入worklist也是在一個另外的goroutine中進行的,這是爲了避免在main goroutine和crawler goroutine中同時向另一個goroutine通過channel發送內容時發生死鎖(因爲另一邊的接收操作還沒有準備好)。當然,這里我們也可以用buffered channel來解決問題,這里不再贅述。
|
||||
|
||||
現在爬蟲可以高並發地運行起來,並且可以產生一大坨的URL了,不過還是會有倆問題。一個問題是在運行一段時間後可能會齣現在log的錯誤信息裡的:
|
||||
現在爬蟲可以高併發地運行起來,併且可以産生一大坨的URL了,不過還是會有倆問題。一個問題是在運行一段時間後可能會齣現在log的錯誤信息里的:
|
||||
|
||||
|
||||
```
|
||||
@@ -56,13 +56,13 @@ https://golang.org/blog/
|
||||
too many open files
|
||||
...
|
||||
```
|
||||
最初的錯誤信息是一個讓人莫名的DNS査找失敗,卽使這個域名是完全可靠的。而隨後的錯誤信息揭示了原因:這個程序一次性創建了太多網絡連接,超過了每一個進程的打開文件數限製,旣而導緻了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
最初的錯誤信息是一個讓人莫名的DNS査找失敗,卽使這個域名是完全可靠的。而隨後的錯誤信息揭示了原因:這個程序一次性創建了太多網絡連接,超過了每一個進程的打開文件數限製,旣而導致了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
|
||||
這個程序實在是太他媽並行了。無窮無儘地並行化並不是什麽好事情,因爲不管怎麽說,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製並發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來說,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這裡的n是fd的limit-20,一般情況下。這個一個夜店裡限製客人數目是一個道理,隻有當有客人離開時,纔會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
這個程序實在是太他媽併行了。無窮無盡地併行化併不是什麽好事情,因爲不管怎麽説,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製併發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來説,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這里的n是fd的limit-20,一般情況下。這個一個夜店里限製客人數目是一個道理,隻有當有客人離開時,纔會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
|
||||
我們可以用一個有容量限製的buffered channel來控製並發,這類似於操作繫統裡的計數信號量概唸。從概唸上講,channel裡的n個空槽代表n個可以處理內容的token(通行証),從channel裡接收一個值會釋放其中的一個token,並且生成一個新的空槽位。這樣保証了在沒有接收介入時最多有n個發送操作。(這裡可能我們拿channel裡填充的槽來做token更直觀一些,不過還是這樣吧~)。由於channel裡的元素類型並不重要,我們用一個零值的struct{}來作爲其元素。
|
||||
我們可以用一個有容量限製的buffered channel來控製併發,這類似於操作繫統里的計數信號量概念。從概念上講,channel里的n個空槽代表n個可以處理內容的token(通行證),從channel里接收一個值會釋放其中的一個token,併且生成一個新的空槽位。這樣保證了在沒有接收介入時最多有n個發送操作。(這里可能我們拿channel里填充的槽來做token更直觀一些,不過還是這樣吧~)。由於channel里的元素類型併不重要,我們用一個零值的struct{}來作爲其元素。
|
||||
|
||||
讓我們重寫crawl函數,將對links.Extract的調用操作用獲取、釋放token的操作包裹起來,來確保同一時間對其隻有20個調用。信號量數量和其能操作的IO資源數量應保持接近。
|
||||
讓我們重寫crawl函數,將對links.Extract的調用操作用穫取、釋放token的操作包裹起來,來確保同一時間對其隻有20個調用。信號量數量和其能操作的IO資源數量應保持接近。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/crawl2
|
||||
@@ -114,11 +114,11 @@ func main() {
|
||||
|
||||
```
|
||||
|
||||
這個版本中,計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時,我們都會對n進行++操作,在向worklist中發送初始的命令行參數之前,我們也進行過一次++操作。這裡的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止,這時候說明沒活可乾了。
|
||||
這個版本中,計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時,我們都會對n進行++操作,在向worklist中發送初始的命令行參數之前,我們也進行過一次++操作。這里的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止,這時候説明沒活可榦了。
|
||||
|
||||
現在這個並發爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會齣什麽錯,並且在其完成任務時也會正確地終止。
|
||||
現在這個併發爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會齣什麽錯,併且在其完成任務時也會正確地終止。
|
||||
|
||||
下面的程序是避免過度並發的另一種思路。這個版本使用了原來的crawl函數,但沒有使用計數信號量,取而代之用了20個長活的crawler goroutine,這樣來保証最多20個HTTP請求在並發。
|
||||
下面的程序是避免過度併發的另一種思路。這個版本使用了原來的crawl函數,但沒有使用計數信號量,取而代之用了20個長活的crawler goroutine,這樣來保證最多20個HTTP請求在併發。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -152,17 +152,17 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
所有的爬蟲goroutine現在都是被同一個channel-unseenLinks餵飽的了。主goroutine負責拆分它從worklist裡拿到的元素,然後把沒有抓過的經由unseenLinks channel發送給一個爬蟲的goroutine。
|
||||
所有的爬蟲goroutine現在都是被同一個channel-unseenLinks餵飽的了。主goroutine負責拆分它從worklist里拿到的元素,然後把沒有抓過的經由unseenLinks channel發送給一個爬蟲的goroutine。
|
||||
|
||||
seen這個map被限定在main goroutine中;也就是說這個map隻能在main goroutine中進行訪問。類似於其它的信息隱藏方式,這樣的約束可以讓我們從一定程度上保証程序的正確性。例如,內部變量不能夠在函數外部被訪問到;變量(§2.3.4)在沒有被轉義的情況下是無法在函數外部訪問的;一個對象的封裝字段無法被該對象的方法以外的方法訪問到。在所有的情況下,信息隱藏都可以幫助我們約束我們的程序,使其不發生意料之外的情況。
|
||||
seen這個map被限定在main goroutine中;也就是説這個map隻能在main goroutine中進行訪問。類似於其它的信息隱藏方式,這樣的約束可以讓我們從一定程度上保證程序的正確性。例如,內部變量不能夠在函數外部被訪問到;變量(§2.3.4)在沒有被轉義的情況下是無法在函數外部訪問的;一個對象的封裝字段無法被該對象的方法以外的方法訪問到。在所有的情況下,信息隱藏都可以幫助我們約束我們的程序,使其不發生意料之外的情況。
|
||||
|
||||
crawl函數爬到的鏈接在一個專有的goroutine中被發送到worklist中來避免死鎖。爲了節省空間,這個例子的終止問題我們先不進行詳細闡述了。
|
||||
|
||||
練習8.6: 爲並發爬蟲增加深度限製。也就是說,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面纔能被抓取到。
|
||||
練習8.6: 爲併發爬蟲增加深度限製。也就是説,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面纔能被抓取到。
|
||||
|
||||
練習8.7: 完成一個並發程序來創建一個綫上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這裡可以隻取齣現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,齣現在頁面裡的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
練習8.7: 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取齣現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,齣現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
|
||||
|
||||
譯註:
|
||||
拓展閱讀:
|
||||
拓展閲讀:
|
||||
http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
## 8.7. 基於select的多路復用
|
||||
## 8.7. 基於select的多路複用
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.8. 示例: 並發的字典遍曆
|
||||
## 8.8. 示例: 併發的字典遍歷
|
||||
|
||||
在本小節中,我們會創建一個程序來生成指定目録的硬盤使用情況報告,這個程序和Unix裡的du工具比較相似。大多數工作用下面這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目録下的所有入口。
|
||||
在本小節中,我們會創建一個程序來生成指定目録的硬盤使用情況報告,這個程序和Unix里的du工具比較相似。大多數工作用下面這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目録下的所有入口。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du1
|
||||
@@ -28,9 +28,9 @@ func dirents(dir string) []os.FileInfo {
|
||||
}
|
||||
```
|
||||
|
||||
ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目録而言,walkDir會遞歸地調用其自身,並且會對每一個文件也遞歸調用。walkDir函數會向fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
|
||||
ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目録而言,walkDir會遞歸地調用其自身,併且會對每一個文件也遞歸調用。walkDir函數會向fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
|
||||
|
||||
下面的主函數,用了兩個goroutine。後檯的goroutine調用walkDir來遍曆命令行給齣的每一個路徑並最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,並輸齣其和。
|
||||
下面的主函數,用了兩個goroutine。後颱的goroutine調用walkDir來遍歷命令行給齣的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,併輸齣其和。
|
||||
|
||||
|
||||
```go
|
||||
@@ -82,9 +82,9 @@ $ ./du1 $HOME /usr /bin /etc
|
||||
213201 files 62.7 GB
|
||||
```
|
||||
|
||||
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是,如果簡單地把printDiskUsage函數調用移動到循環裡會導緻其打印齣成百上韆的輸齣。
|
||||
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是,如果簡單地把printDiskUsage函數調用移動到循環里會導致其打印齣成百上韆的輸齣。
|
||||
|
||||
下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目録上循環的後檯goroutine在這裡保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select裡的case也就相當於被禁用了。
|
||||
下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目録上循環的後颱goroutine在這里保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select里的case也就相當於被禁用了。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du2
|
||||
@@ -115,7 +115,7 @@ loop:
|
||||
printDiskUsage(nfiles, nbytes) // final totals
|
||||
}
|
||||
```
|
||||
由於我們的程序不再使用range循環,第一個select的case必鬚顯式地判斷fileSizes的channel是不是已經被關閉了,這裡可以用到channel接收的二值形式。如果channel已經被關閉了的話,程序會直接退齣循環。這裡的break語句用到了標簽break,這樣可以同時終結select和for兩個循環;如果沒有用標簽就break的話隻會退齣內層的select循環,而外層的for循環會使之進入下一輪select循環。
|
||||
由於我們的程序不再使用range循環,第一個select的case必鬚顯式地判斷fileSizes的channel是不是已經被關閉了,這里可以用到channel接收的二值形式。如果channel已經被關閉了的話,程序會直接退齣循環。這里的break語句用到了標籤break,這樣可以同時終結select和for兩個循環;如果沒有用標籤就break的話隻會退齣內層的select循環,而外層的for循環會使之進入下一輪select循環。
|
||||
|
||||
現在程序會悠閒地爲我們打印更新流:
|
||||
|
||||
@@ -130,7 +130,7 @@ $ ./du2 -v $HOME /usr /bin /etc
|
||||
213201 files 62.7 GB
|
||||
```
|
||||
|
||||
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做並行化處理沒什麽別的原因,無非是因爲磁盤繫統並行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做併行化處理沒什麽别的原因,無非是因爲磁盤繫統併行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du3
|
||||
@@ -164,7 +164,7 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
}
|
||||
```
|
||||
|
||||
由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他同時打開太多的文件,就像我們在8.7節中的並發爬蟲一樣:
|
||||
由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他同時打開太多的文件,就像我們在8.7節中的併發爬蟲一樣:
|
||||
|
||||
|
||||
```go
|
||||
@@ -179,8 +179,8 @@ func dirents(dir string) []os.FileInfo {
|
||||
|
||||
```
|
||||
|
||||
這個版本比之前那個快了好幾倍,儘管其具體效率還是和你的運行環境,機器配置相關。
|
||||
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
|
||||
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算並顯示齣來。
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算併顯示齣來。
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
## 8.9. 並發的退齣
|
||||
## 8.9. 併發的退齣
|
||||
|
||||
有時候我們需要通知goroutine停止它正在乾的事情,比如一個正在執行計算的web服務,然而它的客戶端已經斷開了和服務端的連接。
|
||||
有時候我們需要通知goroutine停止它正在榦的事情,比如一個正在執行計算的web服務,然而它的客戶端已經斷開了和服務端的連接。
|
||||
|
||||
Go語言並沒有提供在一個goroutine中終止另一個goroutine的方法,由於這樣會導緻goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel裡發送了一個簡單的值,在countdown的goroutine中會把這個值理解爲自己的退齣信號。但是如果我們想要退齣兩個或者任意多個goroutine怎麽辦呢?
|
||||
Go語言併沒有提供在一個goroutine中終止另一個goroutine的方法,由於這樣會導致goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel里發送了一個簡單的值,在countdown的goroutine中會把這個值理解爲自己的退齣信號。但是如果我們想要退齣兩個或者任意多個goroutine怎麽辦呢?
|
||||
|
||||
一種可能的手段是向abort的channel裡發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了,那麽會導緻我們的channel裡的事件數比goroutine還多,這樣導緻我們的發送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel裡的數目又太少了,所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運行着的。另外,當一個goroutine從abort channel中接收到一個值的時候,他會消費掉這個值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播齣去,這樣goroutine們能夠看到這條事件消息,並且在事件完成之後,可以知道這件事已經發生過了。
|
||||
一種可能的手段是向abort的channel里發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了,那麽會導致我們的channel里的事件數比goroutine還多,這樣導致我們的發送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel里的數目又太少了,所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運行着的。另外,當一個goroutine從abort channel中接收到一個值的時候,他會消費掉這個值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播齣去,這樣goroutine們能夠看到這條事件消息,併且在事件完成之後,可以知道這件事已經發生過了。
|
||||
|
||||
迴憶一下我們關閉了一個channel並且被消費掉了所有已發送的值,操作channel之後的代碼可以立卽被執行,並且會產生零值。我們可以將這個機製擴展一下,來作爲我們的廣播機製:不要向channel發送值,而是用關閉一個channel來進行廣播。
|
||||
迴憶一下我們關閉了一個channel併且被消費掉了所有已發送的值,操作channel之後的代碼可以立卽被執行,併且會産生零值。我們可以將這個機製擴展一下,來作爲我們的廣播機製:不要向channel發送值,而是用關閉一個channel來進行廣播。
|
||||
|
||||
隻要一些小脩改,我們就可以把退齣邏輯加入到前一節的du程序。首先,我們創建一個退齣的channel,這個channel不會向其中發送任何值,但其所在的閉包內要寫明程序需要退齣。我們同時還定義了一個工具函數,cancelled,這個函數在被調用的時候會輪詢退齣狀態。
|
||||
|
||||
@@ -34,7 +34,7 @@ go func() {
|
||||
}()
|
||||
```
|
||||
|
||||
現在我們需要使我們的goroutine來對取消進行響應。在main goroutine中,我們添加了select的第三個case語句,嘗試從done channel中接收內容。如果這個case被滿足的話,在select到的時候卽會返迴,但在結束之前我們需要把fileSizes channel中的內容“排”空,在channel被關閉之前,捨棄掉所有值。這樣可以保証對walkDir的調用不要被向fileSizes發送信息阻塞住,可以正確地完成。
|
||||
現在我們需要使我們的goroutine來對取消進行響應。在main goroutine中,我們添加了select的第三個case語句,嚐試從done channel中接收內容。如果這個case被滿足的話,在select到的時候卽會返迴,但在結束之前我們需要把fileSizes channel中的內容“排”空,在channel被關閉之前,舍棄掉所有值。這樣可以保證對walkDir的調用不要被向fileSizes發送信息阻塞住,可以正確地完成。
|
||||
|
||||
```go
|
||||
for {
|
||||
@@ -51,7 +51,7 @@ for {
|
||||
}
|
||||
```
|
||||
|
||||
walkDir這個goroutine一啟動就會輪詢取消狀態,如果取消狀態被設置的話會直接返迴,並且不做額外的事情。這樣我們將所有在取消事件之後創建的goroutine改變爲無操作。
|
||||
walkDir這個goroutine一啟動就會輪詢取消狀態,如果取消狀態被設置的話會直接返迴,併且不做額外的事情。這樣我們將所有在取消事件之後創建的goroutine改變爲無操作。
|
||||
|
||||
```go
|
||||
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
@@ -66,9 +66,9 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
|
||||
```
|
||||
|
||||
在walkDir函數的循環中我們對取消狀態進行輪詢可以帶來明顯的益處,可以避免在取消事件發生時還去創建goroutine。取消本身是有一些代價的;想要快速的響應需要對程序邏輯進行侵入式的脩改。確保在取消發生之後不要有代價太大的操作可能會需要脩改你代碼裡的很多地方,但是在一些重要的地方去檢査取消事件也確實能帶來很大的好處。
|
||||
在walkDir函數的循環中我們對取消狀態進行輪詢可以帶來明顯的益處,可以避免在取消事件發生時還去創建goroutine。取消本身是有一些代價的;想要快速的響應需要對程序邏輯進行侵入式的脩改。確保在取消發生之後不要有代價太大的操作可能會需要脩改你代碼里的很多地方,但是在一些重要的地方去檢査取消事件也確實能帶來很大的好處。
|
||||
|
||||
對這個程序的一個簡單的性能分析可以揭示瓶頸在dirents函數中獲取一個信號量。下面的select可以讓這種操作可以被取消,並且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
|
||||
對這個程序的一個簡單的性能分析可以揭示瓶頸在dirents函數中穫取一個信號量。下面的select可以讓這種操作可以被取消,併且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
|
||||
|
||||
```go
|
||||
func dirents(dir string) []os.FileInfo {
|
||||
@@ -82,10 +82,10 @@ func dirents(dir string) []os.FileInfo {
|
||||
}
|
||||
```
|
||||
|
||||
現在當取消發生時,所有後檯的goroutine都會迅速停止並且主函數會返迴。當然,當主函數返迴時,一個程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這裡有一個方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一個panic,然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這裡的一個調研還是很有必要的。我們用panic來獲取到足夠的信息來驗証我們上面的判斷,看看最終到底是什麽樣的情況。
|
||||
現在當取消發生時,所有後颱的goroutine都會迅速停止併且主函數會返迴。當然,當主函數返迴時,一個程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這里有一個方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一個panic,然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這里的一個調研還是很有必要的。我們用panic來穫取到足夠的信息來驗證我們上面的判斷,看看最終到底是什麽樣的情況。
|
||||
|
||||
練習8.10: HTTP請求可能會因http.Request結構體中Cancel channel的關閉而取消。脩改8.6節中的web crawler來支持取消http請求。
|
||||
|
||||
提示: http.Get並沒有提供方便地定製一個請求的方法。你可以用http.NewRequest來取而代之,設置它的Cancel字段,然後用http.DefaultClient.Do(req)來進行這個http請求。
|
||||
提示: http.Get併沒有提供方便地定製一個請求的方法。你可以用http.NewRequest來取而代之,設置它的Cancel字段,然後用http.DefaultClient.Do(req)來進行這個http請求。
|
||||
|
||||
練習8.11:緊接着8.4.4中的mirroredQuery流程,實現一個並發請求url的fetch的變種。當第一個請求返迴時,直接取消其它的請求。
|
||||
練習8.11:緊接着8.4.4中的mirroredQuery流程,實現一個併發請求url的fetch的變種。當第一個請求返迴時,直接取消其它的請求。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 第八章 Goroutines和Channels
|
||||
|
||||
並發程序指的是同時做好幾件事情的程序,隨着硬件的發展,並發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畫的同時,還會後檯執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸齣--現在也會用並發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,儘管計算機的性能每年都在增長,但並不是綫性。
|
||||
併發程序指的是同時做好幾件事情的程序,隨着硬件的發展,併發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畵的同時,還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸齣--現在也會用併發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,盡管計算機的性能每年都在增長,但併不是線性。
|
||||
|
||||
Go語言中的並發程序可以用兩種手段來實現。這一章會講解goroutine和channel,其支持“順序進程通信”(communicating sequential processes)或被簡稱爲CSP。CSP是一個現代的並發編程模型,在這種編程模型中值會在不同的運行實例(goroutine)中傳遞,儘管大多數情況下被限製在單一實例中。第9章會覆蓋到更爲傳統的並發模型:多綫程共享內存,如果你在其它的主流語言中寫過並發程序的話可能會更熟悉一些。第9章同時會講一些本章不會深入的並發程序帶來的重要風險和陷阱。
|
||||
Go語言中的併發程序可以用兩種手段來實現。這一章會講解goroutine和channel,其支持“順序進程通信”(communicating sequential processes)或被簡稱爲CSP。CSP是一個現代的併發編程模型,在這種編程模型中值會在不同的運行實例(goroutine)中傳遞,盡管大多數情況下被限製在單一實例中。第9章會覆蓋到更爲傳統的併發模型:多線程共享內存,如果你在其它的主流語言中寫過併發程序的話可能會更熟悉一些。第9章同時會講一些本章不會深入的併發程序帶來的重要風險和陷阱。
|
||||
|
||||
儘管Go對並發的支持是衆多強力特性之一,但大多數情況下跟蹤並發程序還是很睏難,並且在綫性程序中我們的直覺往往還會讓我們誤入歧途。如果這是你第一次接觸並發,那麽我推薦你稍微多花一些時間來思考這兩個章節中的樣例。
|
||||
盡管Go對併發的支持是衆多強力特性之一,但大多數情況下跟蹤併發程序還是很睏難,併且在線性程序中我們的直覺往往還會讓我們誤入歧途。如果這是你第一次接觸併發,那麽我推薦你稍微多花一些時間來思考這兩個章節中的樣例。
|
||||
|
||||
Reference in New Issue
Block a user