make loop

This commit is contained in:
chai2010
2015-12-26 20:05:30 +08:00
parent 82ec0c025d
commit e15e88dad7
74 changed files with 207 additions and 207 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語句本身會迅速地完成。
@@ -43,7 +43,7 @@ func fib(x int) int {
動畵顯示了幾秒之後fib(45)的調用成功地返迴,併且打印結果:
Fibonacci(45) = 1134903170
然後主函數返迴。當主函數返迴時所有的goroutine都會直接打斷程序退。除了從主函數退或者直接退程序之外沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行但是我們之後可以看到可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine併讓其自己結束執行。
然後主函數返迴。當主函數返迴時所有的goroutine都會直接打斷程序退。除了從主函數退或者直接退程序之外沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行但是我們之後可以看到可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine併讓其自己結束執行。
註意這里的兩個獨立的單元是如何進行組合的spinning和菲波那契的計算。每一個都是寫在獨立的函數中但是每一個函數都會併發地執行。

View File

@@ -48,7 +48,7 @@ Listen函數創建了一個net.Listener的對象這個對象會監聽一個
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命令),這個工具可以用來執行網絡連接操作。
@@ -63,7 +63,7 @@ $ nc localhost 8000
^C
```
客戶端將服務器發來的時間顯示了我們用Control+C來中斷客戶端的執行在Unix繫統上你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具你可以用telnet來實現同樣的效果或者也可以用我們下面的這個用go寫的簡單的telnet程序用net.Dial就可以簡單地創建一個TCP連接
客戶端將服務器發來的時間顯示了我們用Control+C來中斷客戶端的執行在Unix繫統上你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具你可以用telnet來實現同樣的效果或者也可以用我們下面的這個用go寫的簡單的telnet程序用net.Dial就可以簡單地創建一個TCP連接
```go
gopl.io/ch8/netcat1
@@ -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
@@ -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

@@ -40,7 +40,7 @@ func main() {
註意這里的crawl所在的goroutine會將link作爲一個顯式的參數傳入來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這里將命令行參數傳入worklist也是在一個另外的goroutine中進行的這是爲了避免在main goroutine和crawler goroutine中同時向另一個goroutine通過channel發送內容時發生死鎖(因爲另一邊的接收操作還沒有準備好)。當然這里我們也可以用buffered channel來解決問題這里不再贅述。
現在爬蟲可以高併發地運行起來併且可以産生一大坨的URL了不過還是會有倆問題。一個問題是在運行一段時間後可能會現在log的錯誤信息里的
現在爬蟲可以高併發地運行起來併且可以産生一大坨的URL了不過還是會有倆問題。一個問題是在運行一段時間後可能會現在log的錯誤信息里的
```
@@ -58,7 +58,7 @@ https://golang.org/blog/
```
最初的錯誤信息是一個讓人莫名的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{}來作爲其元素。
@@ -82,7 +82,7 @@ func crawl(url string) []string {
}
```
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生的鏈接。(當然除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止我們需要在worklist爲空或者沒有crawl的goroutine在運行時退主循環。
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生的鏈接。(當然除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止我們需要在worklist爲空或者沒有crawl的goroutine在運行時退主循環。
```go
@@ -116,7 +116,7 @@ func main() {
這個版本中計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時我們都會對n進行++操作在向worklist中發送初始的命令行參數之前我們也進行過一次++操作。這里的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止這時候説明沒活可榦了。
現在這個併發爬蟲會比5.6節中的深度優先蒐索版快上20倍而且不會什麽錯,併且在其完成任務時也會正確地終止。
現在這個併發爬蟲會比5.6節中的深度優先蒐索版快上20倍而且不會什麽錯,併且在其完成任務時也會正確地終止。
下面的程序是避免過度併發的另一種思路。這個版本使用了原來的crawl函數但沒有使用計數信號量取而代之用了20個長活的crawler goroutine這樣來保證最多20個HTTP請求在併發。
@@ -158,9 +158,9 @@ seen這個map被限定在main goroutine中也就是説這個map隻能在main
crawl函數爬到的鏈接在一個專有的goroutine中被發送到worklist中來避免死鎖。爲了節省空間這個例子的終止問題我們先不進行詳細闡述了。
練習8.6: 爲併發爬蟲增加深度限製。也就是説如果用戶設置了depth=3那麽隻有從首頁跳轉三次以內能夠跳到的頁面能被抓取到。
練習8.6: 爲併發爬蟲增加深度限製。也就是説如果用戶設置了depth=3那麽隻有從首頁跳轉三次以內能夠跳到的頁面能被抓取到。
練習8.7: 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取現在該域下的所有頁面(比如golang.org結尾譯註外鏈的應該就不算了。)當然了,現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
練習8.7: 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取現在該域下的所有頁面(比如golang.org結尾譯註外鏈的應該就不算了。)當然了,現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
譯註:

View File

@@ -30,7 +30,7 @@ func dirents(dir string) []os.FileInfo {
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
@@ -181,6 +181,6 @@ func dirents(dir string) []os.FileInfo {
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
練習8.9: 編寫一個du工具每隔一段時間將root目録下的目録大小計算併顯示來。
練習8.9: 編寫一個du工具每隔一段時間將root目録下的目録大小計算併顯示來。

View File

@@ -1,14 +1,14 @@
## 8.9. 併發的退
## 8.9. 併發的退
有時候我們需要通知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來進行廣播。
隻要一些小脩改,我們就可以把退邏輯加入到前一節的du程序。首先我們創建一個退的channel這個channel不會向其中發送任何值但其所在的閉包內要寫明程序需要退。我們同時還定義了一個工具函數cancelled這個函數在被調用的時候會輪詢退狀態。
隻要一些小脩改,我們就可以把退邏輯加入到前一節的du程序。首先我們創建一個退的channel這個channel不會向其中發送任何值但其所在的閉包內要寫明程序需要退。我們同時還定義了一個工具函數cancelled這個函數在被調用的時候會輪詢退狀態。
```go
gopl.io/ch8/du4
@@ -24,7 +24,7 @@ func cancelled() bool {
}
```
下面我們創建一個從標準輸入流中讀取內容的goroutine這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵)這個goroutine就會把取消消息通過關閉done的channel廣播去。
下面我們創建一個從標準輸入流中讀取內容的goroutine這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵)這個goroutine就會把取消消息通過關閉done的channel廣播去。
```go
// Cancel traversal when input is detected.
@@ -82,7 +82,7 @@ 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請求。

View File

@@ -22,7 +22,7 @@ func main() {
}
```
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發channel的"資格"信息。
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發channel的"資格"信息。
```go
@@ -55,9 +55,9 @@ func broadcaster() {
}
```
broadcaster監聽來自全局的entering和leaving的channel來穫知客戶端的到來和離開事件。當其接收到其中的一個事件時會更新clients集合當該事件是離開行爲時它會關閉客戶端的消息發channel。broadcaster也會監聽全局的消息channel所有的客戶端都會向這個channel中發送消息。當broadcaster接收到什麽消息時就會將其廣播至所有連接到服務端的客戶端。
broadcaster監聽來自全局的entering和leaving的channel來穫知客戶端的到來和離開事件。當其接收到其中的一個事件時會更新clients集合當該事件是離開行爲時它會關閉客戶端的消息發channel。broadcaster也會監聽全局的消息channel所有的客戶端都會向這個channel中發送消息。當broadcaster接收到什麽消息時就會將其廣播至所有連接到服務端的客戶端。
現在讓我們看看每一個客戶端的goroutine。handleConn函數會爲它的客戶端創建一個消息發channel併通過entering channel來通知客戶端的到來。然後它會讀取客戶端發來的每一行文本併通過全局的消息channel來將這些文本發送併爲每條消息帶上發送者的前綴來標明消息身份。當客戶端發送完畢後handleConn會通過leaving這個channel來通知客戶端的離開併關閉連接。
現在讓我們看看每一個客戶端的goroutine。handleConn函數會爲它的客戶端創建一個消息發channel併通過entering channel來通知客戶端的到來。然後它會讀取客戶端發來的每一行文本併通過全局的消息channel來將這些文本發送併爲每條消息帶上發送者的前綴來標明消息身份。當客戶端發送完畢後handleConn會通過leaving這個channel來通知客戶端的離開併關閉連接。
```go
func handleConn(conn net.Conn) {
@@ -87,7 +87,7 @@ func clientWriter(conn net.Conn, ch <-chan string) {
}
```
另外handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發消息channel中發送的廣播消息併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
另外handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發消息channel中發送的廣播消息併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
下面演示的是當服務器有兩個活動的客戶端連接併且在兩個窗口中運行的情況使用netcat來聊天
```
@@ -118,4 +118,4 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
練習8.12: 使broadcaster能夠將arrival事件通知當前所有的客戶端。爲了達成這個目的你需要有一個客戶端的集合併且在entering和leaving的channel中記録客戶端的名字。
練習8.13: 使聊天服務器能夠斷開空閒的客戶端連接比如最近五分鐘之後沒有發送任何消息的那些客戶端。提示可以在其它goroutine中調用conn.Close()來解除Read調用就像input.Scanner()所做的那樣。
練習8.14: 脩改聊天服務器的網絡協議這樣每一個客戶端就可以在entering時可以提供它們的名字。將消息前綴由之前的網絡地址改爲這個名字。
練習8.15: 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發channel建立緩衝區這樣大部分的消息便不會被丟掉broadcaster應該用一個非阻塞的send向這個channel中發消息。
練習8.15: 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發channel建立緩衝區這樣大部分的消息便不會被丟掉broadcaster應該用一個非阻塞的send向這個channel中發消息。

View File

@@ -1,6 +1,6 @@
# 第八章 Goroutines和Channels
併發程序指的是同時做好幾件事情的程序隨着硬件的發展併發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畵的同時還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸--現在也會用併發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈盡管計算機的性能每年都在增長但併不是線性。
併發程序指的是同時做好幾件事情的程序隨着硬件的發展併發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畵的同時還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸--現在也會用併發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈盡管計算機的性能每年都在增長但併不是線性。
Go語言中的併發程序可以用兩種手段來實現。這一章會講解goroutine和channel其支持“順序進程通信”(communicating sequential processes)或被簡稱爲CSP。CSP是一個現代的併發編程模型在這種編程模型中值會在不同的運行實例(goroutine)中傳遞盡管大多數情況下被限製在單一實例中。第9章會覆蓋到更爲傳統的併發模型多線程共享內存如果你在其它的主流語言中寫過併發程序的話可能會更熟悉一些。第9章同時會講一些本章不會深入的併發程序帶來的重要風險和陷阱。