mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
回到简体
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
## 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語句本身會迅速地完成。
|
||||
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。
|
||||
|
||||
```go
|
||||
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个元素值。由于计算函数使用低效的递归,所以会运行相当长时间,在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行,所以来做一个动画的小图标:
|
||||
|
||||
<u><i>gopl.io/ch8/spinner</i><u>
|
||||
```go
|
||||
@@ -39,12 +39,12 @@ func fib(x int) int {
|
||||
}
|
||||
```
|
||||
|
||||
動畵顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果:
|
||||
动画显示了几秒之后,fib(45)的调用成功地返回,并且打印结果:
|
||||
|
||||
```
|
||||
Fibonacci(45) = 1134903170
|
||||
```
|
||||
|
||||
然後主函數返迴。主函數返迴時,所有的goroutine都會被直接打斷,程序退出。除了從主函數退出或者直接終止程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是之後可以看到一種方式來實現這個目的,通過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包的一部分。
|
||||
|
||||
我們的第一個例子是一個順序執行的時鐘服務器,它會每隔一秒鐘將當前時間寫到客戶端:
|
||||
我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:
|
||||
|
||||
<u><i>gopl.io/ch8/clock1</i></u>
|
||||
```go
|
||||
@@ -45,13 +45,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調用關閉服務器側的連接,然後返迴到主函數,繼續等待下一個連接請求。
|
||||
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
|
||||
@@ -64,7 +64,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连接:
|
||||
|
||||
<u><i>gopl.io/ch8/netcat1</i></u>
|
||||
```go
|
||||
@@ -94,7 +94,7 @@ func mustCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
```
|
||||
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸出中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這里可以開兩個終端窗口,下面左邊的是其中的一個的輸出,右邊的是另一個的輸出:
|
||||
这个程序会从连接中读取数据,并将读到的内容写到标准输出中,直到遇到end of file的条件或者发生错误。mustCopy这个函数我们在本节的几个例子中都会用到。让我们同时运行两个客户端来进行一个测试,这里可以开两个终端窗口,下面左边的是其中的一个的输出,右边的是另一个的输出:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/netcat1
|
||||
@@ -110,9 +110,9 @@ $ ./netcat1
|
||||
$ killall clock1
|
||||
```
|
||||
|
||||
killall命令是一個Unix命令行工具,可以用給定的進程名來殺掉所有名字匹配的進程。
|
||||
killall命令是一个Unix命令行工具,可以用给定的进程名来杀掉所有名字匹配的进程。
|
||||
|
||||
第二個客戶端必須等待第一個客戶端完成工作,這樣服務端才能繼續向後執行;因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動,使其支持併發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
第二个客户端必须等待第一个客户端完成工作,这样服务端才能继续向后执行;因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动,使其支持并发:在handleConn函数调用的地方增加go关键字,让每一次handleConn的调用都进入一个独立的goroutine。
|
||||
|
||||
<u><i>gopl.io/ch8/clock2</i></u>
|
||||
```go
|
||||
@@ -127,7 +127,7 @@ for {
|
||||
|
||||
```
|
||||
|
||||
現在多個客戶端可以同時接收到時間了:
|
||||
现在多个客户端可以同时接收到时间了:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/clock2
|
||||
@@ -147,7 +147,7 @@ $ ./netcat1
|
||||
$ killall clock2
|
||||
```
|
||||
|
||||
**練習 8.1:** 脩改clock2來支持傳入參數作爲端口號,然後寫一個clockwall的程序,這個程序可以同時與多個clock服務器通信,從多服務器中讀取時間,併且在一個表格中一次顯示所有服務傳迴的結果,類似於你在某些辦公室里看到的時鐘牆。如果你有地理學上分布式的服務器可以用的話,讓這些服務器跑在不同的機器上面;或者在同一台機器上跑多個不同的實例,這些實例監聽不同的端口,假裝自己在不同的時區。像下面這樣:
|
||||
**练习 8.1:** 修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多服务器中读取时间,并且在一个表格中一次显示所有服务传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:
|
||||
|
||||
```
|
||||
$ TZ=US/Eastern ./clock2 -port 8010 &
|
||||
@@ -156,4 +156,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
|
||||
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
|
||||
```
|
||||
|
||||
**練習 8.2:** 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列出目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
**练习 8.2:** 实现一个并发FTP服务器。服务器应该解析客户端来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.3. 示例: 併發的Echo服務
|
||||
## 8.3. 示例: 并发的Echo服务
|
||||
|
||||
clock服務器每一個連接都會起一個goroutine。在本節中我們會創建一個echo服務器,這個服務在每個連接中會有多個goroutine。大多數echo服務僅僅會返迴他們讀取到的內容,就像下面這個簡單的handleConn函數所做的一樣:
|
||||
clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器,这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容,就像下面这个简单的handleConn函数所做的一样:
|
||||
|
||||
```go
|
||||
func handleConn(c net.Conn) {
|
||||
@@ -9,7 +9,7 @@ func handleConn(c net.Conn) {
|
||||
}
|
||||
```
|
||||
|
||||
一個更有意思的echo服務應該模擬一個實際的echo的“迴響”,併且一開始要用大寫HELLO來表示“聲音很大”,之後經過一小段延遲返迴一個有所緩和的Hello,然後一個全小寫字母的hello表示聲音漸漸變小直至消失,像下面這個版本的handleConn(譯註:笑看作者腦洞大開):
|
||||
一个更有意思的echo服务应该模拟一个实际的echo的“回响”,并且一开始要用大写HELLO来表示“声音很大”,之后经过一小段延迟返回一个有所缓和的Hello,然后一个全小写字母的hello表示声音渐渐变小直至消失,像下面这个版本的handleConn(译注:笑看作者脑洞大开):
|
||||
|
||||
<u><i>gopl.io/ch8/reverb1</i></u>
|
||||
```go
|
||||
@@ -31,7 +31,7 @@ func handleConn(c net.Conn) {
|
||||
}
|
||||
```
|
||||
|
||||
我們需要陞級我們的客戶端程序,這樣它就可以發送終端的輸入到服務器,併把服務端的返迴輸出到終端上,這使我們有了使用併發的另一個好機會:
|
||||
我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会:
|
||||
|
||||
<u><i>gopl.io/ch8/netcat2</i></u>
|
||||
```go
|
||||
@@ -46,10 +46,10 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
當main goroutine從標準輸入流中讀取內容併將其發送給服務器時,另一個goroutine會讀取併打印服務端的響應。當main goroutine碰到輸入終止時,例如,用戶在終端中按了Control-D(^D),在windows上是Control-Z,這時程序就會被終止,盡管其它goroutine中還有進行中的任務。(在8.4.1中引入了channels後我們會明白如何讓程序等待兩邊都結束)。
|
||||
当main goroutine从标准输入流中读取内容并将其发送给服务器时,另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时,例如,用户在终端中按了Control-D(^D),在windows上是Control-Z,这时程序就会被终止,尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束)。
|
||||
|
||||
下面這個會話中,客戶端的輸入是左對齊的,服務端的響應會用縮進來區别顯示。
|
||||
客戶端會向服務器“喊三次話”:
|
||||
下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。
|
||||
客户端会向服务器“喊三次话”:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/reverb1
|
||||
@@ -72,7 +72,7 @@ yooo-hooo!
|
||||
$ killall reverb1
|
||||
```
|
||||
|
||||
註意客戶端的第三次shout在前一個shout處理完成之前一直沒有被處理,這貌似看起來不是特别“現實”。眞實世界里的迴響應該是會由三次shout的迴聲組合而成的。爲了模擬眞實世界的迴響,我們需要更多的goroutine來做這件事情。這樣我們就再一次地需要go這個關鍵詞了,這次我們用它來調用echo:
|
||||
注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理,这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响,我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了,这次我们用它来调用echo:
|
||||
|
||||
<u><i>gopl.io/ch8/reverb2</i></u>
|
||||
```go
|
||||
@@ -86,8 +86,8 @@ func handleConn(c net.Conn) {
|
||||
}
|
||||
```
|
||||
|
||||
go後跟的函數的參數會在go語句自身執行時被求值;因此input.Text()會在main goroutine中被求值。
|
||||
現在迴響是併發併且會按時間來覆蓋掉其它響應了:
|
||||
go后跟的函数的参数会在go语句自身执行时被求值;因此input.Text()会在main goroutine中被求值。
|
||||
现在回响是并发并且会按时间来覆盖掉其它响应了:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/reverb2
|
||||
@@ -105,4 +105,4 @@ Yooo-hooo!
|
||||
$ killall reverb2
|
||||
```
|
||||
|
||||
讓服務使用併發不隻是處理多個客戶端的請求,甚至在處理單個連接時也可能會用到,就像我們上面的兩個go關鍵詞的用法。然而在我們使用go關鍵詞的同時,需要慎重地考慮net.Conn中的方法在併發地調用時是否安全,事實上對於大多數類型來説也確實不安全。我們會在下一章中詳細地探討併發安全性。
|
||||
让服务使用并发不只是处理多个客户端的请求,甚至在处理单个连接时也可能会用到,就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
### 8.4.1. 不帶緩存的Channels
|
||||
### 8.4.1. 不带缓存的Channels
|
||||
|
||||
一個基於無緩存Channels的發送操作將導致發送者goroutine阻塞,直到另一個goroutine在相同的Channels上執行接收操作,當發送的值通過Channels成功傳輸之後,兩個goroutine可以繼續執行後面的語句。反之,如果接收操作先發生,那麽接收者goroutine也將阻塞,直到有另一個goroutine在相同的Channels上執行發送操作。
|
||||
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
|
||||
|
||||
基於無緩存Channels的發送和接收操作將導致兩個goroutine做一次同步操作。因爲這個原因,無緩存Channels有時候也被稱爲同步Channels。當通過一個無緩存Channels發送數據時,接收者收到數據發生在喚醒發送者goroutine之前(譯註:*happens before*,這是Go語言併發內存模型的一個關鍵術語!)。
|
||||
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前(译注:*happens before*,这是Go语言并发内存模型的一个关键术语!)。
|
||||
|
||||
在討論併發編程時,當我們説x事件在y事件之前發生(*happens before*),我們併不是説x事件在時間上比y時間更早;我們要表達的意思是要保證在此之前的事件都已經完成了,例如在此之前的更新某些變量的操作已經完成,你可以放心依賴這些已完成的事件了。
|
||||
在讨论并发编程时,当我们说x事件在y事件之前发生(*happens before*),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。
|
||||
|
||||
當我們説x事件旣不是在y事件之前發生也不是在y事件之後發生,我們就説x事件和y事件是併發的。這併不是意味着x事件和y事件就一定是同時發生的,我們隻是不能確定這兩個事件發生的先後順序。在下一章中我們將看到,當兩個goroutine併發訪問了相同的變量時,我們有必要保證某些事件的執行順序,以避免出現某些併發問題。
|
||||
当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到,当两个goroutine并发访问了相同的变量时,我们有必要保证某些事件的执行顺序,以避免出现某些并发问题。
|
||||
|
||||
在8.3節的客戶端程序,它在主goroutine中(譯註:就是執行main函數的goroutine)將標準輸入複製到server,因此當客戶端程序關閉標準輸入時,後台goroutine可能依然在工作。我們需要讓主goroutine等待後台goroutine完成工作後再退出,我們使用了一個channel來同步兩個goroutine:
|
||||
在8.3节的客户端程序,它在主goroutine中(译注:就是执行main函数的goroutine)将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine:
|
||||
|
||||
<u><i>gopl.io/ch8/netcat3</i></u>
|
||||
```Go
|
||||
@@ -29,11 +29,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
當用戶關閉了標準輸入,主goroutine中的mustCopy函數調用將返迴,然後調用conn.Close()關閉讀和寫方向的網絡連接。關閉網絡鏈接中的寫方向的鏈接將導致server程序收到一個文件(end-of-file)結束的信號。關閉網絡鏈接中讀方向的鏈接將導致後台goroutine的io.Copy函數調用返迴一個“read from closed connection”(“從關閉的鏈接讀”)類似的錯誤,因此我們臨時移除了錯誤日誌語句;在練習8.3將會提供一個更好的解決方案。(需要註意的是go語句調用了一個函數字面量,這Go語言中啟動goroutine常用的形式。)
|
||||
当用户关闭了标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络链接中的写方向的链接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络链接中读方向的链接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”(“从关闭的链接读”)类似的错误,因此我们临时移除了错误日志语句;在练习8.3将会提供一个更好的解决方案。(需要注意的是go语句调用了一个函数字面量,这Go语言中启动goroutine常用的形式。)
|
||||
|
||||
在後台goroutine返迴之前,它先打印一個日誌信息,然後向done對應的channel發送一個值。主goroutine在退出前先等待從done對應的channel接收一個值。因此,總是可以在程序退出前正確輸出“done”消息。
|
||||
在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此,总是可以在程序退出前正确输出“done”消息。
|
||||
|
||||
基於channels發送消息有兩個重要方面。首先每個消息都有一個值,但是有時候通訊的事實和發生的時刻也同樣重要。當我們更希望強調通訊發生的時刻時,我們將它稱爲**消息事件**。有些消息事件併不攜帶額外的信息,它僅僅是用作兩個goroutine之間的同步,這時候我們可以用`struct{}`空結構體作爲channels元素的類型,雖然也可以使用bool或int類型實現同樣的功能,`done <- 1`語句也比`done <- struct{}{}`更短。
|
||||
基于channels发送消息有两个重要方面。首先每个消息都有一个值,但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时,我们将它称为**消息事件**。有些消息事件并不携带额外的信息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用`struct{}`空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,`done <- 1`语句也比`done <- struct{}{}`更短。
|
||||
|
||||
**練習 8.3:** 在netcat3例子中,conn雖然是一個interface類型的值,但是其底層眞實類型是`*net.TCPConn`,代表一個TCP鏈接。一個TCP鏈接有讀和寫兩個部分,可以使用CloseRead和CloseWrite方法分别關閉它們。脩改netcat3的主goroutine代碼,隻關閉網絡鏈接中寫的部分,這樣的話後台goroutine可以在標準輸入被關閉後繼續打印從reverb1服務器傳迴的數據。(要在reverb2服務器也完成同樣的功能是比較睏難的;參考**練習 8.4**。)
|
||||
**练习 8.3:** 在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是`*net.TCPConn`,代表一个TCP链接。一个TCP链接有读和写两个部分,可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络链接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考**练习 8.4**。)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
### 8.4.2. 串聯的Channels(Pipeline)
|
||||
### 8.4.2. 串联的Channels(Pipeline)
|
||||
|
||||
Channels也可以用於將多個goroutine鏈接在一起,一個Channels的輸出作爲下一個Channels的輸入。這種串聯的Channels就是所謂的管道(pipeline)。下面的程序用兩個channels將三個goroutine串聯起來,如圖8.1所示。
|
||||
Channels也可以用于将多个goroutine链接在一起,一个Channels的输出作为下一个Channels的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来,如图8.1所示。
|
||||
|
||||

|
||||
|
||||
第一個goroutine是一個計數器,用於生成0、1、2、……形式的整數序列,然後通過channel將該整數序列發送給第二個goroutine;第二個goroutine是一個求平方的程序,對收到的每個整數求平方,然後將平方後的結果通過第二個channel發送給第三個goroutine;第三個goroutine是一個打印程序,打印收到的每個整數。爲了保持例子清晰,我們有意選擇了非常簡單的函數,當然三個goroutine的計算很簡單,在現實中確實沒有必要爲如此簡單的運算構建三個goroutine。
|
||||
第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数,当然三个goroutine的计算很简单,在现实中确实没有必要为如此简单的运算构建三个goroutine。
|
||||
|
||||
<u><i>gopl.io/ch8/pipeline1</i></u>
|
||||
```Go
|
||||
@@ -34,17 +34,17 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
如您所料,上面的程序將生成0、1、4、9、……形式的無窮數列。像這樣的串聯Channels的管道(Pipelines)可以用在需要長時間運行的服務中,每個長時間運行的goroutine可能會包含一個死循環,在不同goroutine的死循環內部使用串聯的Channels來通信。但是,如果我們希望通過Channels隻發送有限的數列該如何處理呢?
|
||||
如您所料,上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢?
|
||||
|
||||
如果發送者知道,沒有更多的值需要發送到channel的話,那麽讓接收者也能及時知道沒有多餘的值可接收將是有用的,因爲接收者可以停止不必要的接收等待。這可以通過內置的close函數來關閉channel實現:
|
||||
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:
|
||||
|
||||
```Go
|
||||
close(naturals)
|
||||
```
|
||||
|
||||
當一個channel被關閉後,再向該channel發送數據將導致panic異常。當一個被關閉的channel中已經發送的數據都被成功接收後,後續的接收操作將不再阻塞,它們會立卽返迴一個零值。關閉上面例子中的naturals變量對應的channel併不能終止循環,它依然會收到一個永無休止的零值序列,然後將它們發送給打印者goroutine。
|
||||
当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine。
|
||||
|
||||
沒有辦法直接測試一個channel是否被關閉,但是接收操作有一個變體形式:它多接收一個結果,多接收的第二個結果是一個布爾值ok,ture表示成功從channels接收到值,false表示channels已經被關閉併且里面沒有值可接收。使用這個特性,我們可以脩改squarer函數中的循環代碼,當naturals對應的channel被關閉併沒有值可接收時跳出循環,併且也關閉squares對應的channel.
|
||||
没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel.
|
||||
|
||||
```Go
|
||||
// Squarer
|
||||
@@ -60,9 +60,9 @@ go func() {
|
||||
}()
|
||||
```
|
||||
|
||||
因爲上面的語法是笨拙的,而且這種處理模式很場景,因此Go語言的range循環可直接在channels上面迭代。使用range循環是上面處理模式的簡潔語法,它依次從channel接收數據,當channel被關閉併且沒有值可接收時跳出循環。
|
||||
因为上面的语法是笨拙的,而且这种处理模式很场景,因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。
|
||||
|
||||
在下面的改進中,我們的計數器goroutine隻生成100個含數字的序列,然後關閉naturals對應的channel,這將導致計算平方數的squarer對應的goroutine可以正常終止循環併關閉squares對應的channel。(在一個更複雜的程序中,可以通過defer語句關閉對應的channel。)最後,主goroutine也可以正常終止循環併退出程序。
|
||||
在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。
|
||||
|
||||
<u><i>gopl.io/ch8/pipeline2</i></u>
|
||||
```Go
|
||||
@@ -93,8 +93,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
其實你併不需要關閉每一個channel。隻要當需要告訴接收者goroutine,所有的數據已經全部發送時才需要關閉channel。不管一個channel是否被關閉,當它沒有被引用時將會被Go語言的垃圾自動迴收器迴收。(不要將關閉一個打開文件的操作和關閉一個channel操作混淆。對於每個打開的文件,都需要在不使用的使用調用對應的Close方法來關閉文件。)
|
||||
其实你并不需要关闭每一个channel。只要当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的使用调用对应的Close方法来关闭文件。)
|
||||
|
||||
視圖重複關閉一個channel將導致panic異常,視圖關閉一個nil值的channel也將導致panic異常。關閉一個channels還會觸發一個廣播機製,我們將在8.9節討論。
|
||||
视图重复关闭一个channel将导致panic异常,视图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制,我们将在8.9节讨论。
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 8.4.3. 單方向的Channel
|
||||
### 8.4.3. 单方向的Channel
|
||||
|
||||
隨着程序的增長,人們習慣於將大的函數拆分爲小的函數。我們前面的例子中使用了三個goroutine,然後用兩個channels連鏈接它們,它們都是main函數的局部變量。將三個goroutine拆分爲以下三個函數是自然的想法:
|
||||
随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels连链接它们,它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法:
|
||||
|
||||
```Go
|
||||
func counter(out chan int)
|
||||
@@ -8,15 +8,15 @@ func squarer(out, in chan int)
|
||||
func printer(in chan int)
|
||||
```
|
||||
|
||||
其中squarer計算平方的函數在兩個串聯Channels的中間,因此擁有兩個channels類型的參數,一個用於輸入一個用於輸出。每個channels都用有相同的類型,但是它們的使用方式想反:一個隻用於接收,另一個隻用於發送。參數的名字in和out已經明確表示了這個意圖,但是併無法保證squarer函數向一個in參數對應的channels發送數據或者從一個out參數對應的channels接收數據。
|
||||
其中squarer计算平方的函数在两个串联Channels的中间,因此拥有两个channels类型的参数,一个用于输入一个用于输出。每个channels都用有相同的类型,但是它们的使用方式想反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channels发送数据或者从一个out参数对应的channels接收数据。
|
||||
|
||||
這種場景是典型的。當一個channel作爲一個函數參數是,它一般總是被專門用於隻發送或者隻接收。
|
||||
这种场景是典型的。当一个channel作为一个函数参数是,它一般总是被专门用于只发送或者只接收。
|
||||
|
||||
爲了表明這種意圖併防止被濫用,Go語言的類型繫統提供了單方向的channel類型,分别用於隻發送或隻接收的channel。類型`chan<- int`表示一個隻發送int的channel,隻能發送不能接收。相反,類型`<-chan int`表示一個隻接收int的channel,隻能接收不能發送。(箭頭`<-`和關鍵字chan的相對位置表明了channel的方向。)這種限製將在編譯期檢測。
|
||||
为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型`chan<- int`表示一个只发送int的channel,只能发送不能接收。相反,类型`<-chan int`表示一个只接收int的channel,只能接收不能发送。(箭头`<-`和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。
|
||||
|
||||
因爲關閉操作隻用於斷言不再向channel發送新的數據,所以隻有在發送者所在的goroutine才會調用close函數,因此對一個隻接收的channel調用close將是一個編譯錯誤。
|
||||
因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。
|
||||
|
||||
這是改進的版本,這一次參數使用了單方向channel類型:
|
||||
这是改进的版本,这一次参数使用了单方向channel类型:
|
||||
|
||||
<u><i>gopl.io/ch8/pipeline3</i></u>
|
||||
```Go
|
||||
@@ -49,6 +49,6 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
調用counter(naturals)將導致將`chan int`類型的naturals隱式地轉換爲`chan<- int`類型隻發送型的channel。調用printer(squares)也會導致相似的隱式轉換,這一次是轉換爲`<-chan int`類型隻接收型的channel。任何雙向channel向單向channel變量的賦值操作都將導致該隱式轉換。這里併沒有反向轉換的語法:也就是不能一個將類似`chan<- int`類型的單向型的channel轉換爲`chan int`類型的雙向型的channel。
|
||||
调用counter(naturals)将导致将`chan int`类型的naturals隐式地转换为`chan<- int`类型只发送型的channel。调用printer(squares)也会导致相似的隐式转换,这一次是转换为`<-chan int`类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能一个将类似`chan<- int`类型的单向型的channel转换为`chan int`类型的双向型的channel。
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### 8.4.4. 帶緩存的Channels
|
||||
### 8.4.4. 带缓存的Channels
|
||||
|
||||
帶緩存的Channel內部持有一個元素隊列。隊列的最大容量是在調用make函數創建channel時通過第二個參數指定的。下面的語句創建了一個可以持有三個字符串元素的帶緩存Channel。圖8.2是ch變量對應的channel的圖形表示形式。
|
||||
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。
|
||||
|
||||
```Go
|
||||
ch = make(chan string, 3)
|
||||
@@ -8,9 +8,9 @@ ch = make(chan string, 3)
|
||||
|
||||

|
||||
|
||||
向緩存Channel的發送操作就是向內部緩存隊列的尾部插入元素,接收操作則是從隊列的頭部刪除元素。如果內部緩存隊列是滿的,那麽發送操作將阻塞直到因另一個goroutine執行接收操作而釋放了新的隊列空間。相反,如果channel是空的,接收操作將阻塞直到有另一個goroutine執行發送操作而向隊列插入元素。
|
||||
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
|
||||
|
||||
我們可以在無阻塞的情況下連續向新創建的channel發送三個值:
|
||||
我们可以在无阻塞的情况下连续向新创建的channel发送三个值:
|
||||
|
||||
```Go
|
||||
ch <- "A"
|
||||
@@ -18,42 +18,42 @@ ch <- "B"
|
||||
ch <- "C"
|
||||
```
|
||||
|
||||
此刻,channel的內部緩存隊列將是滿的(圖8.3),如果有第四個發送操作將發生阻塞。
|
||||
此刻,channel的内部缓存队列将是满的(图8.3),如果有第四个发送操作将发生阻塞。
|
||||
|
||||

|
||||
|
||||
如果我們接收一個值,
|
||||
如果我们接收一个值,
|
||||
|
||||
```Go
|
||||
fmt.Println(<-ch) // "A"
|
||||
```
|
||||
|
||||
那麽channel的緩存隊列將不是滿的也不是空的(圖8.4),因此對該channel執行的發送或接收操作都不會發送阻塞。通過這種方式,channel的緩存隊列解耦了接收和發送的goroutine。
|
||||
那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发送阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。
|
||||
|
||||

|
||||
|
||||
在某些特殊情況下,程序可能需要知道channel內部緩存的容量,可以用內置的cap函數獲取:
|
||||
在某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:
|
||||
|
||||
```Go
|
||||
fmt.Println(cap(ch)) // "3"
|
||||
```
|
||||
|
||||
同樣,對於內置的len函數,如果傳入的是channel,那麽將返迴channel內部緩存隊列中有效元素的個數。因爲在併發程序中該信息會隨着接收操作而失效,但是它對某些故障診斷和性能優化會有幫助。
|
||||
同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效,但是它对某些故障诊断和性能优化会有帮助。
|
||||
|
||||
```Go
|
||||
fmt.Println(len(ch)) // "2"
|
||||
```
|
||||
|
||||
在繼續執行兩次接收操作後channel內部的緩存隊列將又成爲空的,如果有第四個接收操作將發生阻塞:
|
||||
在继续执行两次接收操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞:
|
||||
|
||||
```Go
|
||||
fmt.Println(<-ch) // "B"
|
||||
fmt.Println(<-ch) // "C"
|
||||
```
|
||||
|
||||
在這個例子中,發送和接收操作都發生在同一個goroutine中,但是在眞是的程序中它們一般由不同的goroutine執行。Go語言新手有時候會將一個帶緩存的channel當作同一個goroutine中的隊列使用,雖然語法看似簡單,但實際上這是一個錯誤。Channel和goroutine的調度器機製是緊密相連的,一個發送操作——或許是整個程序——可能會永遠阻塞。如果你隻是需要一個簡單的隊列,使用slice就可以了。
|
||||
在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真是的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,一个发送操作——或许是整个程序——可能会永远阻塞。如果你只是需要一个简单的队列,使用slice就可以了。
|
||||
|
||||
下面的例子展示了一個使用了帶緩存channel的應用。它併發地向三個鏡像站點發出請求,三個鏡像站點分散在不同的地理位置。它們分别將收到的響應發送到帶緩存channel,最後接收者隻接收第一個收到的響應,也就是最快的那個響應。因此mirroredQuery函數可能在另外兩個響應慢的鏡像站點響應之前就返迴了結果。(順便説一下,多個goroutines併發地向同一個channel發送數據,或從同一個channel接收數據都是常見的用法。)
|
||||
下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(顺便说一下,多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。)
|
||||
|
||||
```Go
|
||||
func mirroredQuery() string {
|
||||
@@ -67,18 +67,18 @@ func mirroredQuery() string {
|
||||
func request(hostname string) (response string) { /* ... */ }
|
||||
```
|
||||
|
||||
如果我們使用了無緩存的channel,那麽兩個慢的goroutines將會因爲沒有人接收而被永遠卡住。這種情況,稱爲goroutines洩漏,這將是一個BUG。和垃圾變量不同,洩漏的goroutines併不會被自動迴收,因此確保每個不再需要的goroutine能正常退出是重要的。
|
||||
如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
|
||||
|
||||
關於無緩存或帶緩存channels之間的選擇,或者是帶緩存channels的容量大小的選擇,都可能影響程序的正確性。無緩存channel更強地保證了每個發送操作與相應的同步接收操作;但是對於帶緩存channel,這些操作是解耦的。同樣,卽使我們知道將要發送到一個channel的信息的數量上限,創建一個對應容量大小帶緩存channel也是不現實的,因爲這要求在執行任何接收操作之前緩存所有已經發送的值。如果未能分配足夠的緩衝將導致程序死鎖。
|
||||
关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。
|
||||
|
||||
Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個廚師,一個烘焙,一個上糖衣,還有一個將每個蛋糕傳遞到它下一個廚師在生産線。在狹小的廚房空間環境,每個廚師在完成蛋糕後必須等待下一個廚師已經準備好接受它;這類似於在一個無緩存的channel上進行溝通。
|
||||
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师在生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进行沟通。
|
||||
|
||||
如果在每個廚師之間有一個放置一個蛋糕的額外空間,那麽每個廚師就可以將一個完成的蛋糕臨時放在那里而馬上進入下一個蛋糕在製作中;這類似於將channel的緩存隊列的容量設置爲1。隻要每個廚師的平均工作效率相近,那麽其中大部分的傳輸工作將是迅速的,個體之間細小的效率差異將在交接過程中瀰補。如果廚師之間有更大的額外空間——也是就更大容量的緩存隊列——將可以在不停止生産線的前提下消除更大的效率波動,例如一個廚師可以短暫地休息,然後在加快趕上進度而不影響其其他人。
|
||||
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕在制作中;这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后在加快赶上进度而不影响其其他人。
|
||||
|
||||
另一方面,如果生産線的前期階段一直快於後續階段,那麽它們之間的緩存在大部分時間都將是滿的。相反,如果後續階段比前期階段更快,那麽它們之間的緩存在大部分時間都將是空的。對於這類場景,額外的緩存併沒有帶來任何好處。
|
||||
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。
|
||||
|
||||
生産線的隱喻對於理解channels和goroutines的工作機製是很有幫助的。例如,如果第二階段是需要精心製作的複雜操作,一個廚師可能無法跟上第一個廚師的進度,或者是無法滿足第階段廚師的需求。要解決這個問題,我們可以雇傭另一個廚師來幫助完成第二階段的工作,他執行相同的任務但是獨立工作。這類似於基於相同的channels創建另一個獨立的goroutine。
|
||||
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第阶段厨师的需求。要解决这个问题,我们可以雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
|
||||
|
||||
我們沒有太多的空間展示全部細節,但是gopl.io/ch8/cake包模擬了這個蛋糕店,可以通過不同的參數調整。它還對上面提到的幾種場景提供對應的基準測試(§11.4) 。
|
||||
我们没有太多的空间展示全部细节,但是gopl.io/ch8/cake包模拟了这个蛋糕店,可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试(§11.4) 。
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
## 8.4. Channels
|
||||
|
||||
如果説goroutine是Go語音程序的併發體的話,那麽channels它們之間的通信機製。一個channels是一個通信機製,它可以讓一個goroutine通過它給另一個goroutine發送值信息。每個channel都有一個特殊的類型,也就是channels可發送數據的類型。一個可以發送int類型數據的channel一般寫爲chan int。
|
||||
如果说goroutine是Go语音程序的并发体的话,那么channels它们之间的通信机制。一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
|
||||
|
||||
使用內置的make函數,我們可以創建一個channel:
|
||||
使用内置的make函数,我们可以创建一个channel:
|
||||
|
||||
```Go
|
||||
ch := make(chan int) // ch has type 'chan int'
|
||||
```
|
||||
|
||||
和map類似,channel也一個對應make創建的底層數據結構的引用。當我們複製一個channel或用於函數參數傳遞時,我們隻是拷貝了一個channel引用,因此調用者何被調用者將引用同一個channel對象。和其它的引用類型一樣,channel的零值也是nil。
|
||||
和map类似,channel也一个对应make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者何被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
|
||||
|
||||
兩個相同類型的channel可以使用==運算符比較。如果兩個channel引用的是相通的對象,那麽比較的結果爲眞。一個channel也可以和nil進行比較。
|
||||
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
|
||||
|
||||
一個channel有發送和接受兩個主要操作,都是通信行爲。一個發送語句將一個值從一個goroutine通過channel發送到另一個執行接收操作的goroutine。發送和接收兩個操作都是用`<-`運算符。在發送語句中,`<-`運算符分割channel和要發送的值。在接收語句中,`<-`運算符寫在channel對象之前。一個不使用接收結果的接收操作也是合法的。
|
||||
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都是用`<-`运算符。在发送语句中,`<-`运算符分割channel和要发送的值。在接收语句中,`<-`运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。
|
||||
|
||||
```Go
|
||||
ch <- x // a send statement
|
||||
@@ -20,15 +20,15 @@ x = <-ch // a receive expression in an assignment statement
|
||||
<-ch // a receive statement; result is discarded
|
||||
```
|
||||
|
||||
Channel還支持close操作,用於關閉channel,隨後對基於該channel的任何發送操作都將導致panic異常。對一個已經被close過的channel之行接收操作依然可以接受到之前已經成功發送的數據;如果channel中已經沒有數據的話講産生一個零值的數據。
|
||||
Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel之行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话讲产生一个零值的数据。
|
||||
|
||||
使用內置的close函數就可以關閉一個channel:
|
||||
使用内置的close函数就可以关闭一个channel:
|
||||
|
||||
```Go
|
||||
close(ch)
|
||||
```
|
||||
|
||||
以最簡單方式調用make函數創建的時一個無緩存的channel,但是我們也可以指定第二個整形參數,對應channel的容量。如果channel的容量大於零,那麽該channel就是帶緩存的channel。
|
||||
以最简单方式调用make函数创建的时一个无缓存的channel,但是我们也可以指定第二个整形参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。
|
||||
|
||||
```Go
|
||||
ch = make(chan int) // unbuffered channel
|
||||
@@ -36,7 +36,7 @@ ch = make(chan int, 0) // unbuffered channel
|
||||
ch = make(chan int, 3) // buffered channel with capacity 3
|
||||
```
|
||||
|
||||
我們將先討論無緩存的channel,然後在8.4.4節討論帶緩存的channel。
|
||||
我们将先讨论无缓存的channel,然后在8.4.4节讨论带缓存的channel。
|
||||
|
||||
|
||||
{% include "./ch8-04-1.md" %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.5. 併發的循環
|
||||
## 8.5. 并发的循环
|
||||
|
||||
本節中,我們會探索一些用來在併行時循環迭代的常見併發模型。我們會探究從全尺寸圖片生成一些縮略圖的問題。gopl.io/ch8/thumbnail包提供了ImageFile函數來幫我們拉伸圖片。我們不會説明這個函數的實現,隻需要從gopl.io下載它。
|
||||
本节中,我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现,只需要从gopl.io下载它。
|
||||
|
||||
<u><i>gopl.io/ch8/thumbnail</i></u>
|
||||
```go
|
||||
@@ -12,7 +12,7 @@ package thumbnail
|
||||
func ImageFile(infile string) (string, error)
|
||||
```
|
||||
|
||||
下面的程序會循環迭代一些圖片文件名,併爲每一張圖片生成一個縮略圖:
|
||||
下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图:
|
||||
|
||||
<u><i>gopl.io/ch8/thumbnail</i></u>
|
||||
```go
|
||||
@@ -26,9 +26,9 @@ func makeThumbnails(filenames []string) {
|
||||
}
|
||||
```
|
||||
|
||||
顯然我們處理文件的順序無關緊要,因爲每一個圖片的拉伸操作和其它圖片的處理操作都是彼此獨立的。像這種子問題都是完全彼此獨立的問題被叫做易併行問題(譯註:embarrassingly parallel,直譯的話更像是尷尬併行)。易併行問題是最容易被實現成併行的一類問題(廢話),併且是最能夠享受併發帶來的好處,能夠隨着併行的規模線性地擴展。
|
||||
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注:embarrassingly parallel,直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且是最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
|
||||
|
||||
下面讓我們併行地執行這些操作,從而將文件IO的延遲隱藏掉,併用上多核cpu的計算能力來拉伸圖像。我們的第一個併發程序隻是使用了一個go關鍵字。這里我們先忽略掉錯誤,之後再進行處理。
|
||||
下面让我们并行地执行这些操作,从而将文件IO的延迟隐藏掉,并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误,之后再进行处理。
|
||||
|
||||
```go
|
||||
// NOTE: incorrect!
|
||||
@@ -39,9 +39,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.
|
||||
@@ -60,7 +60,7 @@ func makeThumbnails3(filenames []string) {
|
||||
}
|
||||
```
|
||||
|
||||
註意我們將f的值作爲一個顯式的變量傳給了函數,而不是在循環的閉包中聲明:
|
||||
注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明:
|
||||
|
||||
```go
|
||||
for _, f := range filenames {
|
||||
@@ -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.
|
||||
@@ -135,9 +135,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.
|
||||
@@ -174,14 +174,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是最合适的呢?
|
||||
|
||||
@@ -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中的是一样的。
|
||||
|
||||
<u><i>gopl.io/ch8/crawl1</i></u>
|
||||
```go
|
||||
@@ -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/
|
||||
...
|
||||
```
|
||||
|
||||
最初的錯誤信息是一個讓人莫名的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资源数量应保持接近。
|
||||
|
||||
<u><i>gopl.io/ch8/crawl2</i></u>
|
||||
```go
|
||||
@@ -82,7 +82,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生出的鏈接。(當然,除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退出主循環。
|
||||
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然,除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题)。为了使这个程序能够终止,我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -111,11 +111,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() {
|
||||
@@ -149,16 +149,16 @@ 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中來避免死鎖。爲了節省空間,這個例子的終止問題我們先不進行詳細闡述了。
|
||||
crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省空间,这个例子的终止问题我们先不进行详细阐述了。
|
||||
|
||||
**練習 8.6:** 爲併發爬蟲增加深度限製。也就是説,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面才能被抓取到。
|
||||
**练习 8.6:** 为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。
|
||||
|
||||
**練習 8.7:** 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取出現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,出現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
**练习 8.7:** 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org结尾,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。
|
||||
|
||||
|
||||
**譯註:**
|
||||
拓展閲讀 [Handling 1 Million Requests per Minute with Go](http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/)。
|
||||
**译注:**
|
||||
拓展阅读 [Handling 1 Million Requests per Minute with Go](http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.7. 基於select的多路複用
|
||||
## 8.7. 基于select的多路复用
|
||||
|
||||
下面的程序會進行火箭發射的倒計時。time.Tick函數返迴一個channel,程序會週期性地像一個節拍器一樣向這個channel發送事件。每一個事件的值是一個時間戳,不過更有意思的是其傳送方式。
|
||||
下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel,程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳,不过更有意思的是其传送方式。
|
||||
|
||||
<u><i>gopl.io/ch8/countdown1</i></u>
|
||||
```go
|
||||
@@ -15,7 +15,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
現在我們讓這個程序支持在倒計時中,用戶按下return鍵時直接中斷發射流程。首先,我們啟動一個goroutine,這個goroutine會嚐試從標準輸入中調入一個單獨的byte併且,如果成功了,會向名爲abort的channel發送一個值。
|
||||
现在我们让这个程序支持在倒计时中,用户按下return键时直接中断发射流程。首先,我们启动一个goroutine,这个goroutine会尝试从标准输入中调入一个单独的byte并且,如果成功了,会向名为abort的channel发送一个值。
|
||||
|
||||
<u><i>gopl.io/ch8/countdown2</i></u>
|
||||
```go
|
||||
@@ -26,7 +26,7 @@ go func() {
|
||||
}()
|
||||
```
|
||||
|
||||
現在每一次計數循環的迭代都需要等待兩個channel中的其中一個返迴事件了:ticker channel當一切正常時(就像NASA jorgon的"nominal",譯註:這梗估計我們是不懂了)或者異常時返迴的abort事件。我們無法做到從每一個channel中接收信息,如果我們這麽做的話,如果第一個channel中沒有事件發過來那麽程序就會立刻被阻塞,這樣我們就無法收到第二個channel中發過來的事件。這時候我們需要多路複用(multiplex)這些操作了,爲了能夠多路複用,我們使用了select語句。
|
||||
现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了:ticker channel当一切正常时(就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了,为了能够多路复用,我们使用了select语句。
|
||||
|
||||
```go
|
||||
select {
|
||||
@@ -41,11 +41,11 @@ default:
|
||||
}
|
||||
```
|
||||
|
||||
上面是select語句的一般形式。和switch語句稍微有點相似,也會有幾個case和最後的default選擇支。每一個case代表一個通信操作(在某個channel上進行發送或者接收)併且會包含一些語句組成的一個語句塊。一個接收表達式可能隻包含接收表達式自身(譯註:不把接收到的值賦值給變量什麽的),就像上面的第一個case,或者包含在一個簡短的變量聲明中,像第二個case里一樣;第二種形式讓你能夠引用接收到的值。
|
||||
上面是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和最后的default选择支。每一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的),就像上面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式让你能够引用接收到的值。
|
||||
|
||||
select會等待case中有能夠執行的case時去執行。當條件滿足時,select才會去通信併執行case之後的語句;這時候其它通信是不會執行的。一個沒有任何case的select語句寫作select{},會永遠地等待下去。
|
||||
select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
|
||||
|
||||
讓我們迴到我們的火箭發射程序。time.After函數會立卽返迴一個channel,併起一個新的goroutine在經過特定的時間後向該channel發送一個獨立的值。下面的select語句會會一直等待到兩個事件中的一個到達,無論是abort事件或者一個10秒經過的事件。如果10秒經過了還沒有abort事件進入,那麽火箭就會發射。
|
||||
让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会会一直等待到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入,那么火箭就会发射。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -64,7 +64,7 @@ func main() {
|
||||
```
|
||||
|
||||
|
||||
下面這個例子更微秒。ch這個channel的buffer大小是1,所以會交替的爲空或爲滿,所以隻有一個case可以進行下去,無論i是奇數或者偶數,它都會打印0 2 4 6 8。
|
||||
下面这个例子更微秒。ch这个channel的buffer大小是1,所以会交替的为空或为满,所以只有一个case可以进行下去,无论i是奇数或者偶数,它都会打印0 2 4 6 8。
|
||||
|
||||
```go
|
||||
ch := make(chan int, 1)
|
||||
@@ -77,9 +77,9 @@ for i := 0; i < 10; i++ {
|
||||
}
|
||||
```
|
||||
|
||||
如果多個case同時就緒時,select會隨機地選擇一個執行,這樣來保證每一個channel都有平等的被select的機會。增加前一個例子的buffer大小會使其輸出變得不確定,因爲當buffer旣不爲滿也不爲空時,select語句的執行情況就像是拋硬幣的行爲一樣是隨機的。
|
||||
如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定,因为当buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。
|
||||
|
||||
下面讓我們的發射程序打印倒計時。這里的select語句會使每次循環迭代等待一秒來執行退出操作。
|
||||
下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。
|
||||
|
||||
<u><i>gopl.io/ch8/countdown3</i></u>
|
||||
```go
|
||||
@@ -102,9 +102,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
time.Tick函數表現得好像它創建了一個在循環中調用time.Sleep的goroutine,每次被喚醒時發送一個事件。當countdown函數返迴時,它會停止從tick中接收事件,但是ticker這個goroutine還依然存活,繼續徒勞地嚐試從channel中發送值,然而這時候已經沒有其它的goroutine會從該channel中接收值了--這被稱爲goroutine洩露(§8.4.4)。
|
||||
time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine,每次被唤醒时发送一个事件。当countdown函数返回时,它会停止从tick中接收事件,但是ticker这个goroutine还依然存活,继续徒劳地尝试从channel中发送值,然而这时候已经没有其它的goroutine会从该channel中接收值了--这被称为goroutine泄露(§8.4.4)。
|
||||
|
||||
Tick函數挺方便,但是隻有當程序整個生命週期都需要這個時間時我們使用它才比較合適。否則的話,我們應該使用下面的這種模式:
|
||||
Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用下面的这种模式:
|
||||
|
||||
```go
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
@@ -112,9 +112,9 @@ ticker := time.NewTicker(1 * time.Second)
|
||||
ticker.Stop() // cause the ticker's goroutine to terminate
|
||||
```
|
||||
|
||||
有時候我們希望能夠從channel中發送或者接收值,併避免因爲發送或者接收導致的阻塞,尤其是當channel沒有準備好寫或者讀時。select語句就可以實現這樣的功能。select會有一個default來設置當其它的操作都不能夠馬上被處理時程序需要執行哪些邏輯。
|
||||
有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。
|
||||
|
||||
下面的select語句會在abort channel中有值時,從其中接收值;無值時什麽都不做。這是一個非阻塞的接收操作;反複地做這樣的操作叫做“輪詢channel”。
|
||||
下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。
|
||||
|
||||
```go
|
||||
select {
|
||||
@@ -126,8 +126,8 @@ default:
|
||||
}
|
||||
```
|
||||
|
||||
channel的零值是nil。也許會讓你覺得比較奇怪,nil的channel有時候也是有一些用處的。因爲對一個nil的channel發送和接收操作會永遠阻塞,在select語句中操作nil的channel永遠都不會被select到。
|
||||
channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。
|
||||
|
||||
這使得我們可以用nil來激活或者禁用case,來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
|
||||
这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。
|
||||
|
||||
**練習 8.8:** 使用select來改造8.3節中的echo服務器,爲其增加超時,這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。
|
||||
**练习 8.8:** 使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 8.8. 示例: 併發的字典遍歷
|
||||
## 8.8. 示例: 并发的字典遍历
|
||||
|
||||
在本小節中,我們會創建一個程序來生成指定目録的硬盤使用情況報告,這個程序和Unix里的du工具比較相似。大多數工作用下面這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目録下的所有入口。
|
||||
在本小节中,我们会创建一个程序来生成指定目录的硬盘使用情况报告,这个程序和Unix里的du工具比较相似。大多数工作用下面这个walkDir函数来完成,这个函数使用dirents函数来枚举一个目录下的所有入口。
|
||||
|
||||
<u><i>gopl.io/ch8/du1</i></u>
|
||||
```go
|
||||
@@ -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
|
||||
package main
|
||||
@@ -74,7 +74,7 @@ func printDiskUsage(nfiles, nbytes int64) {
|
||||
}
|
||||
```
|
||||
|
||||
這個程序會在打印其結果之前卡住很長時間。
|
||||
这个程序会在打印其结果之前卡住很长时间。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/du1
|
||||
@@ -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也就相当于被禁用了。
|
||||
|
||||
<u><i>gopl.io/ch8/du2</i></u>
|
||||
```go
|
||||
@@ -116,9 +116,9 @@ loop:
|
||||
}
|
||||
```
|
||||
|
||||
由於我們的程序不再使用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循环。
|
||||
|
||||
現在程序會悠閒地爲我們打印更新流:
|
||||
现在程序会悠闲地为我们打印更新流:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/du2
|
||||
@@ -131,7 +131,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关闭。
|
||||
|
||||
<u><i>gopl.io/ch8/du3</i></u>
|
||||
```go
|
||||
@@ -165,7 +165,7 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
}
|
||||
```
|
||||
|
||||
由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他同時打開太多的文件,就像我們在8.7節中的併發爬蟲一樣:
|
||||
由于这个程序在高峰期会创建成百上千的goroutine,我们需要修改dirents函数,用计数信号量来阻止他同时打开太多的文件,就像我们在8.7节中的并发爬虫一样:
|
||||
|
||||
```go
|
||||
// sema is a counting semaphore for limiting concurrency in dirents.
|
||||
@@ -178,6 +178,6 @@ func dirents(dir string) []os.FileInfo {
|
||||
// ...
|
||||
```
|
||||
|
||||
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
|
||||
这个版本比之前那个快了好几倍,尽管其具体效率还是和你的运行环境,机器配置相关。
|
||||
|
||||
**練習 8.9:** 編寫一個du工具,每隔一段時間將root目録下的目録大小計算併顯示出來。
|
||||
**练习 8.9:** 编写一个du工具,每隔一段时间将root目录下的目录大小计算并显示出来。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
## 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,這個函數在被調用的時候會輪詢退出狀態。
|
||||
只要一些小修改,我们就可以把退出逻辑加入到前一节的du程序。首先,我们创建一个退出的channel,这个channel不会向其中发送任何值,但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数,cancelled,这个函数在被调用的时候会轮询退出状态。
|
||||
|
||||
<u><i>gopl.io/ch8/du4</i></u>
|
||||
```go
|
||||
@@ -24,7 +24,7 @@ func cancelled() bool {
|
||||
}
|
||||
```
|
||||
|
||||
下面我們創建一個從標準輸入流中讀取內容的goroutine,這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵),這個goroutine就會把取消消息通過關閉done的channel廣播出去。
|
||||
下面我们创建一个从标准输入流中读取内容的goroutine,这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键),这个goroutine就会把取消消息通过关闭done的channel广播出去。
|
||||
|
||||
```go
|
||||
// Cancel traversal when input is detected.
|
||||
@@ -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) {
|
||||
@@ -65,9 +65,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 {
|
||||
@@ -81,8 +81,8 @@ 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請求。)
|
||||
**练习 8.10:** HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消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,8 +1,8 @@
|
||||
## 8.10. 示例: 聊天服務
|
||||
## 8.10. 示例: 聊天服务
|
||||
|
||||
我們用一個聊天服務器來終結本章節的內容,這個程序可以讓一些用戶通過服務器向其它所有用戶廣播文本消息。這個程序中有四種goroutine。main和broadcaster各自是一個goroutine實例,每一個客戶端的連接都會有一個handleConn和clientWriter的goroutine。broadcaster是select用法的不錯的樣例,因爲它需要處理三種不同類型的消息。
|
||||
我们用一个聊天服务器来终结本章节的内容,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例,每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例,因为它需要处理三种不同类型的消息。
|
||||
|
||||
下面演示的main goroutine的工作,是listen和accept(譯註:網絡編程里的概念)從客戶端過來的連接。對每一個連接,程序都會建立一個新的handleConn的goroutine,就像我們在本章開頭的併發的echo服務器里所做的那樣。
|
||||
下面演示的main goroutine的工作,是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接,程序都会建立一个新的handleConn的goroutine,就像我们在本章开头的并发的echo服务器里所做的那样。
|
||||
|
||||
<u><i>gopl.io/ch8/chat</i></u>
|
||||
```go
|
||||
@@ -23,7 +23,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發出channel的"資格"信息。
|
||||
然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的"资格"信息。
|
||||
|
||||
```go
|
||||
type client chan<- string // an outgoing message channel
|
||||
@@ -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,9 +87,9 @@ func clientWriter(conn net.Conn, ch <-chan string) {
|
||||
}
|
||||
```
|
||||
|
||||
另外,handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發出消息channel中發送的廣播消息,併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
|
||||
另外,handleConn为每一个客户端创建了一个clientWriter的goroutine来接收向客户端发出消息channel中发送的广播消息,并将它们写入到客户端的网络连接。客户端的读取方循环会在broadcaster接收到leaving通知并关闭了channel后终止。
|
||||
|
||||
下面演示的是當服務器有兩個活動的客戶端連接,併且在兩個窗口中運行的情況,使用netcat來聊天:
|
||||
下面演示的是当服务器有两个活动的客户端连接,并且在两个窗口中运行的情况,使用netcat来聊天:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/chat
|
||||
@@ -113,12 +113,12 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
|
||||
127.0.0.1:64211 has left”
|
||||
```
|
||||
|
||||
當與n個客戶端保持聊天session時,這個程序會有2n+2個併發的goroutine,然而這個程序卻併不需要顯式的鎖(§9.2)。clients這個map被限製在了一個獨立的goroutine中,broadcaster,所以它不能被併發地訪問。多個goroutine共享的變量隻有這些channel和net.Conn的實例,兩個東西都是併發安全的。我們會在下一章中更多地解決約束,併發安全以及goroutine中共享變量的含義。
|
||||
当与n个客户端保持聊天session时,这个程序会有2n+2个并发的goroutine,然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中,broadcaster,所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例,两个东西都是并发安全的。我们会在下一章中更多地解决约束,并发安全以及goroutine中共享变量的含义。
|
||||
|
||||
**練習 8.12:** 使broadcaster能夠將arrival事件通知當前所有的客戶端。爲了達成這個目的,你需要有一個客戶端的集合,併且在entering和leaving的channel中記録客戶端的名字。
|
||||
**练习 8.12:** 使broadcaster能够将arrival事件通知当前所有的客户端。为了达成这个目的,你需要有一个客户端的集合,并且在entering和leaving的channel中记录客户端的名字。
|
||||
|
||||
**練習 8.13:** 使聊天服務器能夠斷開空閒的客戶端連接,比如最近五分鐘之後沒有發送任何消息的那些客戶端。提示:可以在其它goroutine中調用conn.Close()來解除Read調用,就像input.Scanner()所做的那樣。
|
||||
**练习 8.13:** 使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。
|
||||
|
||||
**練習 8.14:** 脩改聊天服務器的網絡協議這樣每一個客戶端就可以在entering時可以提供它們的名字。將消息前綴由之前的網絡地址改爲這個名字。
|
||||
**练习 8.14:** 修改聊天服务器的网络协议这样每一个客户端就可以在entering时可以提供它们的名字。将消息前缀由之前的网络地址改为这个名字。
|
||||
|
||||
**練習 8.15:** 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息,而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發出channel建立緩衝區,這樣大部分的消息便不會被丟掉;broadcaster應該用一個非阻塞的send向這個channel中發消息。
|
||||
**练习 8.15:** 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好写。或者为每一个客户端的消息发出channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个非阻塞的send向这个channel中发消息。
|
||||
|
||||
@@ -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