make loop

This commit is contained in:
chai2010
2015-12-18 14:49:31 +08:00
parent 9fde1ff772
commit f9ac065e47
106 changed files with 725 additions and 725 deletions

View File

@@ -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和菲波那契的計算。每一個都是寫在獨立的函數中但是每一個函數都會發地執行。

View File

@@ -1,8 +1,8 @@
## 8.2. 示例: 發的Clock服務
## 8.2. 示例: 發的Clock服務
網絡編程是發大顯身手的一個領域由於服務器是最典型的需要同時處理很多連接的程序這些連接一般來自遠彼此獨立的客戶端。在本小節中我們會講解go語言的net包這個包提供編寫一個網絡客戶端或者服務器程序的基本組件無論兩者間通信是使用TCPUDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包的方法也算是net包的一部分。
網絡編程是發大顯身手的一個領域由於服務器是最典型的需要同時處理很多連接的程序這些連接一般來自遠彼此獨立的客戶端。在本小節中我們會講解go語言的net包這個包提供編寫一個網絡客戶端或者服務器程序的基本組件無論兩者間通信是使用TCPUDP或者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命令來作爲客戶端或者也可以自己實現一個。

View File

@@ -1,3 +1,3 @@
## 8.3. 示例: 發的Echo服務
## 8.3. 示例: 發的Echo服務
TODO

View File

@@ -1,3 +1,3 @@
## 8.5. 行的循環
## 8.5. 行的循環
TODO

View File

@@ -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/

View File

@@ -1,3 +1,3 @@
## 8.7. 基於select的多路
## 8.7. 基於select的多路
TODO

View File

@@ -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類型的sliceos.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目録而言walkDir會遞歸地調用其自身且會對每一個文件也遞歸調用。walkDir函數會向fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
ioutil.ReadDir函數會返迴一個os.FileInfo類型的sliceos.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目録下的目録大小計算顯示齣來。

View File

@@ -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的變種。當第一個請求返迴時直接取消其它的請求。

View File

@@ -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對發的支持是衆多強力特性之一,但大多數情況下跟蹤發程序還是很睏難,且在性程序中我們的直覺往往還會讓我們誤入歧途。如果這是你第一次接觸發,那麽我推薦你稍微多花一些時間來思考這兩個章節中的樣例。