《Go語(yǔ)言圣經(jīng)》學(xué)習(xí)筆記 第八章 Groroutines和Channels
目錄
Goroutines 實(shí)例:并發(fā)的Clock服務(wù) 實(shí)例:并發(fā)的Echo服務(wù) Channels 并發(fā)的循環(huán) 示例:并發(fā)Web爬蟲(chóng) 基于select的多路復(fù)用 示例:并發(fā)的字典遍歷 并發(fā)的退出 示例:聊天服務(wù)
注:學(xué)習(xí)《Go語(yǔ)言圣經(jīng)》筆記,PDF點(diǎn)擊下載,建議看書(shū)。 Go語(yǔ)言小白學(xué)習(xí)筆記,書(shū)上的內(nèi)容照搬,大佬看了勿噴,以后熟悉了會(huì)總結(jié)成自己的讀書(shū)筆記。
并發(fā)程序指同時(shí)進(jìn)行多個(gè)任務(wù)的程序, 隨著硬件的發(fā)展, 并發(fā)程序變得越來(lái)越重要。 Web服務(wù)器會(huì)一次處理成千上萬(wàn)的請(qǐng)求。 平板電腦和手機(jī)app在渲染用戶畫(huà)面同時(shí)還會(huì)后臺(tái)執(zhí)行各種計(jì)算任務(wù)和網(wǎng)絡(luò)請(qǐng)求。 即使是傳統(tǒng)的批處理問(wèn)題–讀取數(shù)據(jù), 計(jì)算, 寫(xiě)輸出–現(xiàn)在也會(huì)用并發(fā)來(lái)隱藏掉I/O的操作延遲以充分利用現(xiàn)代計(jì)算機(jī)設(shè)備的多個(gè)核心。 計(jì)算機(jī)的性能每年都在以非線性的速度增長(zhǎng)。 Go語(yǔ)言中的并發(fā)程序可以用兩種手段來(lái)實(shí)現(xiàn)。 本章講解goroutine和channel, 其支持“順序通信進(jìn)程”(communicating sequential processes)或被簡(jiǎn)稱為CSP。 CSP是一種現(xiàn)代的并發(fā)編程模型, 在這種編程模型中值會(huì)在不同的運(yùn)行實(shí)例(goroutine)中傳遞, 盡管大多數(shù)情況下仍然是被限制在單一實(shí)例中。 第9章覆蓋更為傳統(tǒng)的并發(fā)模型: 多線程共享內(nèi)存, 如果你在其它的主流語(yǔ)言中寫(xiě)過(guò)并發(fā)程序的話可能會(huì)更熟悉一些。 第9章也會(huì)深入介紹一些并發(fā)程序帶來(lái)的風(fēng)險(xiǎn)和陷阱。 盡管Go對(duì)并發(fā)的支持是眾多強(qiáng)力特性之一, 但跟蹤調(diào)試并發(fā)程序還是很困難, 在線性程序中形成的直覺(jué)往往還會(huì)使我們誤入歧途。 如果這是讀者第一次接觸并發(fā), 推薦稍微多花一些時(shí)間來(lái)思考這兩個(gè)章節(jié)中的樣例
1. Goroutines
在Go語(yǔ)言中, 每一個(gè)并發(fā)的執(zhí)行單元叫作一個(gè)goroutine。 設(shè)想這里的一個(gè)程序有兩個(gè)函數(shù),一個(gè)函數(shù)做計(jì)算, 另一個(gè)輸出結(jié)果, 假設(shè)兩個(gè)函數(shù)沒(méi)有相互之間的調(diào)用關(guān)系。 一個(gè)線性的程序會(huì)先調(diào)用其中的一個(gè)函數(shù), 然后再調(diào)用另一個(gè)。 如果程序中包含多個(gè)goroutine, 對(duì)兩個(gè)函數(shù)的調(diào)用則可能發(fā)生在同一時(shí)刻。 馬上就會(huì)看到這樣的一個(gè)程序。 如果你使用過(guò)操作系統(tǒng)或者其它語(yǔ)言提供的線程, 那么你可以簡(jiǎn)單地把goroutine類比作一個(gè)線程, 這樣你就可以寫(xiě)出一些正確的程序了。 goroutine和線程的本質(zhì)區(qū)別會(huì)在9.8節(jié)中講。 當(dāng)一個(gè)程序啟動(dòng)時(shí), 其主函數(shù)即在一個(gè)單獨(dú)的goroutine中運(yùn)行, 我們叫它main goroutine。 新的goroutine會(huì)用go語(yǔ)句來(lái)創(chuàng)建。 在語(yǔ)法上, go語(yǔ)句是一個(gè)普通的函數(shù)或方法調(diào)用前加上關(guān)鍵字go。 go語(yǔ)句會(huì)使其語(yǔ)句中的函數(shù)在一個(gè)新創(chuàng)建的goroutine中運(yùn)行。 而go語(yǔ)句本身會(huì)迅速地完成。 下面的例子, main goroutine將計(jì)算菲波那契數(shù)列的第45個(gè)元素值。 由于計(jì)算函數(shù)使用低效的遞歸, 所以會(huì)運(yùn)行相當(dāng)長(zhǎng)時(shí)間, 在此期間我們想讓用戶看到一個(gè)可見(jiàn)的標(biāo)識(shí)來(lái)表明程序依然在正常運(yùn)行, 所以來(lái)做一個(gè)動(dòng)畫(huà)的小圖標(biāo): gopl.io/ch8/spinner 動(dòng)畫(huà)顯示了幾秒之后, fib(45)的調(diào)用成功地返回, 并且打印結(jié)果: 然后主函數(shù)返回。 主函數(shù)返回時(shí), 所有的goroutine都會(huì)被直接打斷, 程序退出。 除了從主函數(shù)退出或者直接終止程序之外, 沒(méi)有其它的編程方法能夠讓一個(gè)goroutine來(lái)打斷另一個(gè)的執(zhí)行, 但是之后可以看到一種方式來(lái)實(shí)現(xiàn)這個(gè)目的, 通過(guò)goroutine之間的通信來(lái)讓一個(gè)goroutine請(qǐng)求其它的goroutine, 并被請(qǐng)求的goroutine自行結(jié)束執(zhí)行。 留意一下這里的兩個(gè)獨(dú)立的單元是如何進(jìn)行組合的, spinning和菲波那契的計(jì)算。 分別在獨(dú)立的函數(shù)中, 但兩個(gè)函數(shù)會(huì)同時(shí)執(zhí)行。
2. 示例: 并發(fā)的Clock服務(wù)
網(wǎng)絡(luò)編程是并發(fā)大顯身手的一個(gè)領(lǐng)域, 由于服務(wù)器是最典型的需要同時(shí)處理很多連接的程序, 這些連接一般來(lái)自遠(yuǎn)彼此獨(dú)立的客戶端。 在本小節(jié)中, 我們會(huì)講解go語(yǔ)言的net包, 這個(gè)包提供編寫(xiě)一個(gè)網(wǎng)絡(luò)客戶端或者服務(wù)器程序的基本組件, 無(wú)論兩者間通信是使用TCP, UDP或者Unix domain sockets。 在第一章中我們已經(jīng)使用過(guò)的net/http包里的方法, 也算是net包的一部分。
我們的第一個(gè)例子是一個(gè)順序執(zhí)行的時(shí)鐘服務(wù)器, 它會(huì)每隔一秒鐘將當(dāng)前時(shí)間寫(xiě)到客戶端:
gopl.io/ch8/clock1
package mainimport ( "io" "log" "net" "time"
) func main ( ) { listener, err := net. Listen ( "tcp" , "localhost:8082" ) if err != nil { log. Fatal ( err) } for { conn, err := listener. Accept ( ) if err != nil { log. Print ( err) continue } handleConn ( conn) }
} func handleConn ( c net. Conn) { defer c. Close ( ) for { _ , err := io. WriteString ( c, time. Now ( ) . Format ( "15:04:05\n" ) ) if err != nil { return } time. Sleep ( 1 * time. Second) }
} Listen函數(shù)創(chuàng)建了一個(gè)net.Listener的對(duì)象, 這個(gè)對(duì)象會(huì)監(jiān)聽(tīng)一個(gè)網(wǎng)絡(luò)端口上到來(lái)的連接, 在這個(gè)例子里我們用的是TCP的localhost:8000端口。 listener對(duì)象的Accept方法會(huì)直接阻塞, 直到一個(gè)新的連接被創(chuàng)建, 然后會(huì)返回一個(gè)net.Conn對(duì)象來(lái)表示這個(gè)連接。
handleConn函數(shù)會(huì)處理一個(gè)完整的客戶端連接。 在一個(gè)for死循環(huán)中, 將當(dāng)前的時(shí)候用time.Now()函數(shù)得到, 然后寫(xiě)到客戶端。 由于net.Conn實(shí)現(xiàn)了io.Writer接口, 我們可以直接向其寫(xiě)入內(nèi)容。 這個(gè)死循環(huán)會(huì)一直執(zhí)行, 直到寫(xiě)入失敗。 最可能的原因是客戶端主動(dòng)斷開(kāi)連接。 這種情況下handleConn函數(shù)會(huì)用defer調(diào)用關(guān)閉服務(wù)器側(cè)的連接, 然后返回到主函數(shù), 繼續(xù)等待下一個(gè)連接請(qǐng)求。
time.Time.Format方法提供了一種格式化日期和時(shí)間信息的方式。 它的參數(shù)是一個(gè)格式化模板標(biāo)識(shí)如何來(lái)格式化時(shí)間, 而這個(gè)格式化模板限定為Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個(gè)部分(周幾, 月份, 一個(gè)月的第幾天, 等等)。 可以以任意的形式來(lái)組合前面這個(gè)模板;出現(xiàn)在模板中的部分會(huì)作為參考來(lái)對(duì)時(shí)間格式進(jìn)行輸出。 在上面的例子中我們只用到了小時(shí)、 分鐘和秒。 time包里定義了很多標(biāo)準(zhǔn)時(shí)間格式, 比如time.RFC1123。 在進(jìn)行格式化的逆向操作time.Parse時(shí), 也會(huì)用到同樣的策略。 (譯注: 這是go語(yǔ)言和其它語(yǔ)言相比比較奇葩的一個(gè)地方。 。 你需要記住格式化字符串是1月2日下午3點(diǎn)4分5秒零六年UTC-0700, 而不像其它語(yǔ)言那樣Y-m-d H:i:s一樣, 當(dāng)然了這里可以用1234567的方式來(lái)記憶, 倒是也不麻煩)
為了連接例子里的服務(wù)器, 我們需要一個(gè)客戶端程序, 比如netcat這個(gè)工具(nc命令), 這個(gè)工具可以用來(lái)執(zhí)行網(wǎng)絡(luò)連接操作。
客戶端將服務(wù)器發(fā)來(lái)的時(shí)間顯示了出來(lái), 我們用Control+C來(lái)中斷客戶端的執(zhí)行, 在Unix系統(tǒng)上, 你會(huì)看到^C這樣的響應(yīng)。 如果你的系統(tǒng)沒(méi)有裝nc這個(gè)工具, 你可以用telnet來(lái)實(shí)現(xiàn)同樣的效果, 或者也可以用我們下面的這個(gè)用go寫(xiě)的簡(jiǎn)單的telnet程序, 用net.Dial就可以簡(jiǎn)單地創(chuàng)建一個(gè)TCP連接:
gopl.io/ch8/netcat1
這個(gè)程序會(huì)從連接中讀取數(shù)據(jù), 并將讀到的內(nèi)容寫(xiě)到標(biāo)準(zhǔn)輸出中, 直到遇到end of file的條件或者發(fā)生錯(cuò)誤。 mustCopy這個(gè)函數(shù)我們?cè)诒竟?jié)的幾個(gè)例子中都會(huì)用到。 讓我們同時(shí)運(yùn)行兩個(gè)客戶端來(lái)進(jìn)行一個(gè)測(cè)試, 這里可以開(kāi)兩個(gè)終端窗口, 下面左邊的是其中的一個(gè)的輸出, 右邊的是另一個(gè)的輸出:
killall命令是一個(gè)Unix命令行工具, 可以用給定的進(jìn)程名來(lái)殺掉所有名字匹配的進(jìn)程。
第二個(gè)客戶端必須等待第一個(gè)客戶端完成工作, 這樣服務(wù)端才能繼續(xù)向后執(zhí)行; 因?yàn)槲覀冞@里的服務(wù)器程序同一時(shí)間只能處理一個(gè)客戶端連接。 我們這里對(duì)服務(wù)端程序做一點(diǎn)小改動(dòng),使其支持并發(fā): 在handleConn函數(shù)調(diào)用的地方增加go關(guān)鍵字, 讓每一次handleConn的調(diào)用都進(jìn)入一個(gè)獨(dú)立的goroutine。
gopl.io/ch8/clock2
現(xiàn)在多個(gè)客戶端可以同時(shí)接收到時(shí)間了:
3. 示例: 并發(fā)的Echo服務(wù)
clock服務(wù)器每一個(gè)連接都會(huì)起一個(gè)goroutine。 在本節(jié)中我們會(huì)創(chuàng)建一個(gè)echo服務(wù)器, 這個(gè)服務(wù)在每個(gè)連接中會(huì)有多個(gè)goroutine。 大多數(shù)echo服務(wù)僅僅會(huì)返回他們讀取到的內(nèi)容, 就像下面這個(gè)簡(jiǎn)單的handleConn函數(shù)所做的一樣: 一個(gè)更有意思的echo服務(wù)應(yīng)該模擬一個(gè)實(shí)際的echo的“回響”, 并且一開(kāi)始要用大寫(xiě)HELLO來(lái)表示“聲音很大”, 之后經(jīng)過(guò)一小段延遲返回一個(gè)有所緩和的Hello, 然后一個(gè)全小寫(xiě)字母的hello表示聲音漸漸變小直至消失, 像下面這個(gè)版本的handleConn(譯注: 笑看作者腦洞大開(kāi)): gopl.io/ch8/reverb1 我們需要升級(jí)我們的客戶端程序, 這樣它就可以發(fā)送終端的輸入到服務(wù)器, 并把服務(wù)端的返回輸出到終端上, 這使我們有了使用并發(fā)的另一個(gè)好機(jī)會(huì): 當(dāng)main goroutine從標(biāo)準(zhǔn)輸入流中讀取內(nèi)容并將其發(fā)送給服務(wù)器時(shí), 另一個(gè)goroutine會(huì)讀取并打印服務(wù)端的響應(yīng)。 當(dāng)main goroutine碰到輸入終止時(shí), 例如, 用戶在終端中按了ControlD(^D), 在windows上是Control-Z, 這時(shí)程就會(huì)被終止, 盡管其它goroutine中還有進(jìn)行中的任務(wù)。 (在8.4.1中引入了channels后我們會(huì)明白如何讓程序等待兩邊都結(jié)束)。 下面這個(gè)會(huì)話中, 客戶端的輸入是左對(duì)齊的, 服務(wù)端的響應(yīng)會(huì)用縮進(jìn)來(lái)區(qū)別顯示。 客戶端會(huì)向服務(wù)器“喊三次話”: 注意客戶端的第三次shout在前一個(gè)shout處理完成之前一直沒(méi)有被處理, 這貌似看起來(lái)不是特別“現(xiàn)實(shí)”。 真實(shí)世界里的回響應(yīng)該是會(huì)由三次shout的回聲組合而成的。 為了模擬真實(shí)世界的回響, 我們需要更多的goroutine來(lái)做這件事情。 這樣我們就再一次地需要go這個(gè)關(guān)鍵詞了,這次我們用它來(lái)調(diào)用echo: gopl.io/ch8/reverb2 go后跟的函數(shù)的參數(shù)會(huì)在go語(yǔ)句自身執(zhí)行時(shí)被求值; 因此input.Text()會(huì)在main goroutine中被求值。 現(xiàn)在回響是并發(fā)并且會(huì)按時(shí)間來(lái)覆蓋掉其它響應(yīng)了: 讓服務(wù)使用并發(fā)不只是處理多個(gè)客戶端的請(qǐng)求, 甚至在處理單個(gè)連接時(shí)也可能會(huì)用到, 就像我們上面的兩個(gè)go關(guān)鍵詞的用法。 然而在我們使用go關(guān)鍵詞的同時(shí), 需要慎重地考慮net.Conn中的方法在并發(fā)地調(diào)用時(shí)是否安全, 事實(shí)上對(duì)于大多數(shù)類型來(lái)說(shuō)也確實(shí)不安全。 我們會(huì)在下一章中詳細(xì)地探討并發(fā)安全性。
4. Channels
如果說(shuō)goroutine是Go語(yǔ)音程序的并發(fā)體的話, 那么channels它們之間的通信機(jī)制。 一個(gè)channels是一個(gè)通信機(jī)制, 它可以讓一個(gè)goroutine通過(guò)它給另一個(gè)goroutine發(fā)送值信息。 每個(gè)channel都有一個(gè)特殊的類型, 也就是channels可發(fā)送數(shù)據(jù)的類型。 一個(gè)可以發(fā)送int類型數(shù)據(jù)的channel一般寫(xiě)為chan int。 使用內(nèi)置的make函數(shù), 我們可以創(chuàng)建一個(gè)channel: 和map類似, channel也一個(gè)對(duì)應(yīng)make創(chuàng)建的底層數(shù)據(jù)結(jié)構(gòu)的引用。 當(dāng)我們復(fù)制一個(gè)channel或用于函數(shù)參數(shù)傳遞時(shí), 我們只是拷貝了一個(gè)channel引用, 因此調(diào)用者何被調(diào)用者將引用同一個(gè)channel對(duì)象。 和其它的引用類型一樣, channel的零值也是nil。 兩個(gè)相同類型的channel可以使用==運(yùn)算符比較。 如果兩個(gè)channel引用的是相通的對(duì)象, 那么比較的結(jié)果為真。 一個(gè)channel也可以和nil進(jìn)行比較。 一個(gè)channel有發(fā)送和接受兩個(gè)主要操作, 都是通信行為。 一個(gè)發(fā)送語(yǔ)句將一個(gè)值從一個(gè)goroutine通過(guò)channel發(fā)送到另一個(gè)執(zhí)行接收操作的goroutine。 發(fā)送和接收兩個(gè)操作都是用 <- 運(yùn)算符。 在發(fā)送語(yǔ)句中, <- 運(yùn)算符分割channel和要發(fā)送的值。 在接收語(yǔ)句中, <- 運(yùn)算符寫(xiě)在channel對(duì)象之前。 一個(gè)不使用接收結(jié)果的接收操作也是合法的。 Channel還支持close操作, 用于關(guān)閉channel, 隨后對(duì)基于該channel的任何發(fā)送操作都將導(dǎo)致panic異常。 對(duì)一個(gè)已經(jīng)被close過(guò)的channel之行接收操作依然可以接受到之前已經(jīng)成功發(fā)送的數(shù)據(jù); 如果channel中已經(jīng)沒(méi)有數(shù)據(jù)的話講產(chǎn)生一個(gè)零值的數(shù)據(jù)。 使用內(nèi)置的close函數(shù)就可以關(guān)閉一個(gè)channel: 以最簡(jiǎn)單方式調(diào)用make函數(shù)創(chuàng)建的時(shí)一個(gè)無(wú)緩沖的channel, 但是我們也可以指定第二個(gè)整形參數(shù), 對(duì)應(yīng)channel的容量。 如果channel的容量大于零, 那么該channel就是帶緩沖的channel。 我們將先討論無(wú)緩沖的channel, 然后在8.4.4節(jié)討論帶緩沖的channel。
1. 不帶緩存的Channels
一個(gè)基于無(wú)緩存Channels的發(fā)送操作將導(dǎo)致發(fā)送者goroutine阻塞, 直到另一個(gè)goroutine在相同的Channels上執(zhí)行接收操作, 當(dāng)發(fā)送的值通過(guò)Channels成功傳輸之后, 兩個(gè)goroutine可以繼續(xù)執(zhí)行后面的語(yǔ)句。 反之, 如果接收操作先發(fā)生, 那么接收者goroutine也將阻塞, 直到有另一個(gè)goroutine在相同的Channels上執(zhí)行發(fā)送操作。 基于無(wú)緩存Channels的發(fā)送和接收操作將導(dǎo)致兩個(gè)goroutine做一次同步操作。 因?yàn)檫@個(gè)原因, 無(wú)緩存Channels有時(shí)候也被稱為同步Channels。 當(dāng)通過(guò)一個(gè)無(wú)緩存Channels發(fā)送數(shù)據(jù)時(shí), 接收者收到數(shù)據(jù)發(fā)生在喚醒發(fā)送者goroutine之前( 譯注: happens before, 這是Go語(yǔ)言并發(fā)內(nèi)存模型的一個(gè)關(guān)鍵術(shù)語(yǔ)! ) 當(dāng)我們說(shuō)x事件既不是在y事件之前發(fā)生也不是在y事件之后發(fā)生, 我們就說(shuō)x事件和y事件是并發(fā)的。 這并不是意味著x事件和y事件就一定是同時(shí)發(fā)生的, 我們只是不能確定這兩個(gè)事件發(fā)生的先后順序。 在下一章中我們將看到, 當(dāng)兩個(gè)goroutine并發(fā)訪問(wèn)了相同的變量時(shí), 我們有必要保證某些事件的執(zhí)行順序, 以避免出現(xiàn)某些并發(fā)問(wèn)題。 在8.3節(jié)的客戶端程序, 它在主goroutine中( 譯注: 就是執(zhí)行main函數(shù)的goroutine) 將標(biāo)準(zhǔn)輸入復(fù)制到server, 因此當(dāng)客戶端程序關(guān)閉標(biāo)準(zhǔn)輸入時(shí), 后臺(tái)goroutine可能依然在工作。 我們需要讓主goroutine等待后臺(tái)goroutine完成工作后再退出, 我們使用了一個(gè)channel來(lái)同步兩個(gè)goroutine: gopl.io/ch8/netcat3 當(dāng)用戶關(guān)閉了標(biāo)準(zhǔn)輸入, 主goroutine中的mustCopy函數(shù)調(diào)用將返回, 然后調(diào)用conn.Close()關(guān)閉讀和寫(xiě)方向的網(wǎng)絡(luò)連接。 關(guān)閉網(wǎng)絡(luò)鏈接中的寫(xiě)方向的鏈接將導(dǎo)致server程序收到一個(gè)文件( end-of-?le) 結(jié)束的信號(hào)。 關(guān)閉網(wǎng)絡(luò)鏈接中讀方向的鏈接將導(dǎo)致后臺(tái)goroutine的io.Copy函數(shù)調(diào)用返回一個(gè)“read from closed connection”( “從關(guān)閉的鏈接讀”) 類似的錯(cuò)誤, 因此我們臨時(shí)移除了錯(cuò)誤日志語(yǔ)句; 在練習(xí)8.3將會(huì)提供一個(gè)更好的解決方案。 ( 需要注意的是go語(yǔ)句調(diào)用了一個(gè)函數(shù)字面量, 這Go語(yǔ)言中啟動(dòng)goroutine常用的形式。 ) 在后臺(tái)goroutine返回之前, 它先打印一個(gè)日志信息, 然后向done對(duì)應(yīng)的channel發(fā)送一個(gè)值。主goroutine在退出前先等待從done對(duì)應(yīng)的channel接收一個(gè)值。 因此, 總是可以在程序退出前正確輸出“done”消息。 基于channels發(fā)送消息有兩個(gè)重要方面。 首先每個(gè)消息都有一個(gè)值, 但是有時(shí)候通訊的事實(shí)和發(fā)生的時(shí)刻也同樣重要。 當(dāng)我們更希望強(qiáng)調(diào)通訊發(fā)生的時(shí)刻時(shí), 我們將它稱為消息事件。有些消息事件并不攜帶額外的信息, 它僅僅是用作兩個(gè)goroutine之間的同步, 這時(shí)候我們可以用 struct{} 空結(jié)構(gòu)體作為channels元素的類型, 雖然也可以使用bool或int類型實(shí)現(xiàn)同樣的功能, done <- 1 語(yǔ)句也比 done <- struct{}{} 更短。
2. 串聯(lián)的Channels( Pipeline)
Channels也可以用于將多個(gè)goroutine鏈接在一起, 一個(gè)Channels的輸出作為下一個(gè)Channels的輸入。 這種串聯(lián)的Channels就是所謂的管道( pipeline) 。 下面的程序用兩個(gè)channels將三個(gè)goroutine串聯(lián)起來(lái), 如圖8.1所示。 第一個(gè)goroutine是一個(gè)計(jì)數(shù)器, 用于生成0、 1、 2、 ……形式的整數(shù)序列, 然后通過(guò)channel將該整數(shù)序列發(fā)送給第二個(gè)goroutine; 第二個(gè)goroutine是一個(gè)求平方的程序, 對(duì)收到的每個(gè)整數(shù)求平方, 然后將平方后的結(jié)果通過(guò)第二個(gè)channel發(fā)送給第三個(gè)goroutine; 第三個(gè)goroutine是一個(gè)打印程序, 打印收到的每個(gè)整數(shù)。 為了保持例子清晰, 我們有意選擇了非常簡(jiǎn)單的函數(shù), 當(dāng)然三個(gè)goroutine的計(jì)算很簡(jiǎn)單, 在現(xiàn)實(shí)中確實(shí)沒(méi)有必要為如此簡(jiǎn)單的運(yùn)算構(gòu)建三個(gè)goroutine。 gopl.io/ch8/pipeline1 如您所料, 上面的程序?qū)⑸?、 1、 4、 9、 ……形式的無(wú)窮數(shù)列。 像這樣的串聯(lián)Channels的管道( Pipelines) 可以用在需要長(zhǎng)時(shí)間運(yùn)行的服務(wù)中, 每個(gè)長(zhǎng)時(shí)間運(yùn)行的goroutine可能會(huì)包含一個(gè)死循環(huán), 在不同goroutine的死循環(huán)內(nèi)部使用串聯(lián)的Channels來(lái)通信。 但是, 如果我們希望通過(guò)Channels只發(fā)送有限的數(shù)列該如何處理呢? 如果發(fā)送者知道, 沒(méi)有更多的值需要發(fā)送到channel的話, 那么讓接收者也能及時(shí)知道沒(méi)有多余的值可接收將是有用的, 因?yàn)榻邮照呖梢酝V共槐匾慕邮盏却?這可以通過(guò)內(nèi)置的close函數(shù)來(lái)關(guān)閉channel實(shí)現(xiàn): 沒(méi)有辦法直接測(cè)試一個(gè)channel是否被關(guān)閉, 但是接收操作有一個(gè)變體形式: 它多接收一個(gè)結(jié)果, 多接收的第二個(gè)結(jié)果是一個(gè)布爾值ok, ture表示成功從channels接收到值, false表示channels已經(jīng)被關(guān)閉并且里面沒(méi)有值可接收。 使用這個(gè)特性, 我們可以修改squarer函數(shù)中的循環(huán)代碼, 當(dāng)naturals對(duì)應(yīng)的channel被關(guān)閉并沒(méi)有值可接收時(shí)跳出循環(huán), 并且也關(guān)閉squares對(duì)應(yīng)的channel. 因?yàn)樯厦娴恼Z(yǔ)法是笨拙的, 而且這種處理模式很場(chǎng)景, 因此Go語(yǔ)言的range循環(huán)可直接在channels上面迭代。 使用range循環(huán)是上面處理模式的簡(jiǎn)潔語(yǔ)法, 它依次從channel接收數(shù)據(jù), 當(dāng)channel被關(guān)閉并且沒(méi)有值可接收時(shí)跳出循環(huán)。 在下面的改進(jìn)中, 我們的計(jì)數(shù)器goroutine只生成100個(gè)含數(shù)字的序列, 然后關(guān)閉naturals對(duì)應(yīng)的channel, 這將導(dǎo)致計(jì)算平方數(shù)的squarer對(duì)應(yīng)的goroutine可以正常終止循環(huán)并關(guān)閉squares對(duì)應(yīng)的channel。 ( 在一個(gè)更復(fù)雜的程序中, 可以通過(guò)defer語(yǔ)句關(guān)閉對(duì)應(yīng)的channel。 ) 最后, 主goroutine也可以正常終止循環(huán)并退出程序。 opl.io/ch8/pipeline2 其實(shí)你并不需要關(guān)閉每一個(gè)channel。 只要當(dāng)需要告訴接收者goroutine, 所有的數(shù)據(jù)已經(jīng)全部發(fā)送時(shí)才需要關(guān)閉channel。 不管一個(gè)channel是否被關(guān)閉, 當(dāng)它沒(méi)有被引用時(shí)將會(huì)被Go語(yǔ)言的垃圾自動(dòng)回收器回收。 ( 不要將關(guān)閉一個(gè)打開(kāi)文件的操作和關(guān)閉一個(gè)channel操作混淆。 對(duì)于每個(gè)打開(kāi)的文件, 都需要在不使用的使用調(diào)用對(duì)應(yīng)的Close方法來(lái)關(guān)閉文件。 ) 試圖重復(fù)關(guān)閉一個(gè)channel將導(dǎo)致panic異常, 試圖關(guān)閉一個(gè)nil值的channel也將導(dǎo)致panic異常。 關(guān)閉一個(gè)channels還會(huì)觸發(fā)一個(gè)廣播機(jī)制, 我們將在8.9節(jié)討論。
3. 單方向的Channel
隨著程序的增長(zhǎng), 人們習(xí)慣于將大的函數(shù)拆分為小的函數(shù)。 我們前面的例子中使用了三個(gè)goroutine, 然后用兩個(gè)channels連鏈接它們, 它們都是main函數(shù)的局部變量。 將三個(gè)goroutine拆分為以下三個(gè)函數(shù)是自然的想法: 其中squarer計(jì)算平方的函數(shù)在兩個(gè)串聯(lián)Channels的中間, 因此擁有兩個(gè)channels類型的參數(shù), 一個(gè)用于輸入一個(gè)用于輸出。 每個(gè)channels都用有相同的類型, 但是它們的使用方式想反: 一個(gè)只用于接收, 另一個(gè)只用于發(fā)送。 參數(shù)的名字in和out已經(jīng)明確表示了這個(gè)意圖, 但是并無(wú)法保證squarer函數(shù)向一個(gè)in參數(shù)對(duì)應(yīng)的channels發(fā)送數(shù)據(jù)或者從一個(gè)out參數(shù)對(duì)應(yīng)的channels接收數(shù)據(jù)。 這種場(chǎng)景是典型的。 當(dāng)一個(gè)channel作為一個(gè)函數(shù)參數(shù)是, 它一般總是被專門(mén)用于只發(fā)送或者只接收。 為了表明這種意圖并防止被濫用, Go語(yǔ)言的類型系統(tǒng)提供了單方向的channel類型, 分別用于只發(fā)送或只接收的channel。 類型 chan<- int 表示一個(gè)只發(fā)送int的channel, 只能發(fā)送不能接收。 相反, 類型 <-chan int 表示一個(gè)只接收int的channel, 只能接收不能發(fā)送。 ( 箭頭 <- 和關(guān)鍵字chan的相對(duì)位置表明了channel的方向。 ) 這種限制將在編譯期檢測(cè)。 因?yàn)殛P(guān)閉操作只用于斷言不再向channel發(fā)送新的數(shù)據(jù), 所以只有在發(fā)送者所在的goroutine才會(huì)調(diào)用close函數(shù), 因此對(duì)一個(gè)只接收的channel調(diào)用close將是一個(gè)編譯錯(cuò)誤。 這是改進(jìn)的版本, 這一次參數(shù)使用了單方向channel類型: 調(diào)用counter(naturals)將導(dǎo)致將 chan int 類型的naturals隱式地轉(zhuǎn)換為 chan<- int 類型只發(fā)送型的channel。 調(diào)用printer(squares)也會(huì)導(dǎo)致相似的隱式轉(zhuǎn)換, 這一次是轉(zhuǎn)換為 <-chanint 類型只接收型的channel。 任何雙向channel向單向channel變量的賦值操作都將導(dǎo)致該隱式轉(zhuǎn)換。 這里并沒(méi)有反向轉(zhuǎn)換的語(yǔ)法: 也就是不能一個(gè)將類似 chan<- int 類型的單向型的channel轉(zhuǎn)換為 chan int 類型的雙向型的channel
4. 帶緩存的Channels
帶緩存的Channel內(nèi)部持有一個(gè)元素隊(duì)列。 隊(duì)列的最大容量是在調(diào)用make函數(shù)創(chuàng)建channel時(shí)通過(guò)第二個(gè)參數(shù)指定的。 下面的語(yǔ)句創(chuàng)建了一個(gè)可以持有三個(gè)字符串元素的帶緩存Channel。圖8.2是ch變量對(duì)應(yīng)的channel的圖形表示形式。 向緩存Channel的發(fā)送操作就是向內(nèi)部緩存隊(duì)列的尾部插入元素, 接收操作則是從隊(duì)列的頭部刪除元素。 如果內(nèi)部緩存隊(duì)列是滿的, 那么發(fā)送操作將阻塞直到因另一個(gè)goroutine執(zhí)行接收操作而釋放了新的隊(duì)列空間。 相反, 如果channel是空的, 接收操作將阻塞直到有另一個(gè)goroutine執(zhí)行發(fā)送操作而向隊(duì)列插入元素。 我們可以在無(wú)阻塞的情況下連續(xù)向新創(chuàng)建的channel發(fā)送三個(gè)值: 此刻, channel的內(nèi)部緩存隊(duì)列將是滿的( 圖8.3) , 如果有第四個(gè)發(fā)送操作將發(fā)生阻塞。 如果我們接收一個(gè)值, 那么channel的緩存隊(duì)列將不是滿的也不是空的( 圖8.4) , 因此對(duì)該channel執(zhí)行的發(fā)送或接收操作都不會(huì)發(fā)送阻塞。 通過(guò)這種方式, channel的緩存隊(duì)列解耦了接收和發(fā)送的goroutine。 在某些特殊情況下, 程序可能需要知道channel內(nèi)部緩存的容量, 可以用內(nèi)置的cap函數(shù)獲取: 同樣, 對(duì)于內(nèi)置的len函數(shù), 如果傳入的是channel, 那么將返回channel內(nèi)部緩存隊(duì)列中有效元素的個(gè)數(shù)。 因?yàn)樵诓l(fā)程序中該信息會(huì)隨著接收操作而失效, 但是它對(duì)某些故障診斷和性能優(yōu)化會(huì)有幫助。 在繼續(xù)執(zhí)行兩次接收操作后channel內(nèi)部的緩存隊(duì)列將又成為空的, 如果有第四個(gè)接收操作將發(fā)生阻塞: 在這個(gè)例子中, 發(fā)送和接收操作都發(fā)生在同一個(gè)goroutine中, 但是在真是的程序中它們一般由不同的goroutine執(zhí)行。 Go語(yǔ)言新手有時(shí)候會(huì)將一個(gè)帶緩存的channel當(dāng)作同一個(gè)goroutine中的隊(duì)列使用, 雖然語(yǔ)法看似簡(jiǎn)單, 但實(shí)際上這是一個(gè)錯(cuò)誤。 Channel和goroutine的調(diào)度器機(jī)制是緊密相連的, 一個(gè)發(fā)送操作——或許是整個(gè)程序——可能會(huì)永遠(yuǎn)阻塞。 如果你只是需要一個(gè)簡(jiǎn)單的隊(duì)列, 使用slice就可以了。 下面的例子展示了一個(gè)使用了帶緩存channel的應(yīng)用。 它并發(fā)地向三個(gè)鏡像站點(diǎn)發(fā)出請(qǐng)求, 三個(gè)鏡像站點(diǎn)分散在不同的地理位置。 它們分別將收到的響應(yīng)發(fā)送到帶緩存channel, 最后接收者只接收第一個(gè)收到的響應(yīng), 也就是最快的那個(gè)響應(yīng)。 因此mirroredQuery函數(shù)可能在另外兩個(gè)響應(yīng)慢的鏡像站點(diǎn)響應(yīng)之前就返回了結(jié)果。 ( 順便說(shuō)一下, 多個(gè)goroutines并發(fā)地向同一個(gè)channel發(fā)送數(shù)據(jù), 或從同一個(gè)channel接收數(shù)據(jù)都是常見(jiàn)的用法。 ) 如果我們使用了無(wú)緩存的channel, 那么兩個(gè)慢的goroutines將會(huì)因?yàn)闆](méi)有人接收而被永遠(yuǎn)卡住。 這種情況, 稱為goroutines泄漏, 這將是一個(gè)BUG。 和垃圾變量不同, 泄漏的goroutines并不會(huì)被自動(dòng)回收, 因此確保每個(gè)不再需要的goroutine能正常退出是重要的。 關(guān)于無(wú)緩存或帶緩存channels之間的選擇, 或者是帶緩存channels的容量大小的選擇, 都可能影響程序的正確性。 無(wú)緩存channel更強(qiáng)地保證了每個(gè)發(fā)送操作與相應(yīng)的同步接收操作; 但是對(duì)于帶緩存channel, 這些操作是解耦的。 同樣, 即使我們知道將要發(fā)送到一個(gè)channel的信息的數(shù)量上限, 創(chuàng)建一個(gè)對(duì)應(yīng)容量大小帶緩存channel也是不現(xiàn)實(shí)的, 因?yàn)檫@要求在執(zhí)行任何接收操作之前緩存所有已經(jīng)發(fā)送的值。 如果未能分配足夠的緩沖將導(dǎo)致程序死鎖。 Channel的緩存也可能影響程序的性能。 想象一家蛋糕店有三個(gè)廚師, 一個(gè)烘焙, 一個(gè)上糖衣, 還有一個(gè)將每個(gè)蛋糕傳遞到它下一個(gè)廚師在生產(chǎn)線。 在狹小的廚房空間環(huán)境, 每個(gè)廚師在完成蛋糕后必須等待下一個(gè)廚師已經(jīng)準(zhǔn)備好接受它; 這類似于在一個(gè)無(wú)緩存的channel上進(jìn)行溝通。 如果在每個(gè)廚師之間有一個(gè)放置一個(gè)蛋糕的額外空間, 那么每個(gè)廚師就可以將一個(gè)完成的蛋糕臨時(shí)放在那里而馬上進(jìn)入下一個(gè)蛋糕在制作中; 這類似于將channel的緩存隊(duì)列的容量設(shè)置為1。 只要每個(gè)廚師的平均工作效率相近, 那么其中大部分的傳輸工作將是迅速的, 個(gè)體之間細(xì)小的效率差異將在交接過(guò)程中彌補(bǔ)。 如果廚師之間有更大的額外空間——也是就更大容量的緩存隊(duì)列——將可以在不停止生產(chǎn)線的前提下消除更大的效率波動(dòng), 例如一個(gè)廚師可以短暫地休息, 然后在加快趕上進(jìn)度而不影響其其他人。 另一方面, 如果生產(chǎn)線的前期階段一直快于后續(xù)階段, 那么它們之間的緩存在大部分時(shí)間都將是滿的。 相反, 如果后續(xù)階段比前期階段更快, 那么它們之間的緩存在大部分時(shí)間都將是空的。 對(duì)于這類場(chǎng)景, 額外的緩存并沒(méi)有帶來(lái)任何好處。 生產(chǎn)線的隱喻對(duì)于理解channels和goroutines的工作機(jī)制是很有幫助的。 例如, 如果第二階段是需要精心制作的復(fù)雜操作, 一個(gè)廚師可能無(wú)法跟上第一個(gè)廚師的進(jìn)度, 或者是無(wú)法滿足第階段廚師的需求。 要解決這個(gè)問(wèn)題, 我們可以雇傭另一個(gè)廚師來(lái)幫助完成第二階段的工作,他執(zhí)行相同的任務(wù)但是獨(dú)立工作。 這類似于基于相同的channels創(chuàng)建另一個(gè)獨(dú)立的goroutine。 我們沒(méi)有太多的空間展示全部細(xì)節(jié), 但是gopl.io/ch8/cake包模擬了這個(gè)蛋糕店, 可以通過(guò)不同的參數(shù)調(diào)整。 它還對(duì)上面提到的幾種場(chǎng)景提供對(duì)應(yīng)的基準(zhǔn)測(cè)試( §11.4) 。
5. 并發(fā)的循環(huán)
本節(jié)中, 我們會(huì)探索一些用來(lái)在并行時(shí)循環(huán)迭代的常見(jiàn)并發(fā)模型。 我們會(huì)探究從全尺寸圖片生成一些縮略圖的問(wèn)題。 gopl.io/ch8/thumbnail包提供了ImageFile函數(shù)來(lái)幫我們拉伸圖片。 我們不會(huì)說(shuō)明這個(gè)函數(shù)的實(shí)現(xiàn), 只需要從gopl.io下載它。
gopl.io/ch8/thumbnail
下面的程序會(huì)循環(huán)迭代一些圖片文件名, 并為每一張圖片生成一個(gè)縮略圖:
gopl.io/ch8/thumbnail
顯然我們處理文件的順序無(wú)關(guān)緊要, 因?yàn)槊恳粋€(gè)圖片的拉伸操作和其它圖片的處理操作都是彼此獨(dú)立的。 像這種子問(wèn)題都是完全彼此獨(dú)立的問(wèn)題被叫做易并行問(wèn)題(譯注:embarrassingly parallel, 直譯的話更像是尷尬并行)。 易并行問(wèn)題是最容易被實(shí)現(xiàn)成并行的一類問(wèn)題(廢話), 并且是最能夠享受并發(fā)帶來(lái)的好處, 能夠隨著并行的規(guī)模線性地?cái)U(kuò)展。
下面讓我們并行地執(zhí)行這些操作, 從而將文件IO的延遲隱藏掉, 并用上多核cpu的計(jì)算能力來(lái)拉伸圖像。 我們的第一個(gè)并發(fā)程序只是使用了一個(gè)go關(guān)鍵字。 這里我們先忽略掉錯(cuò)誤, 之后再進(jìn)行處理。
這個(gè)版本運(yùn)行的實(shí)在有點(diǎn)太快, 實(shí)際上, 由于它比最早的版本使用的時(shí)間要短得多, 即使當(dāng)文件名的slice中只包含有一個(gè)元素。 這就有點(diǎn)奇怪了, 如果程序沒(méi)有并發(fā)執(zhí)行的話, 那為什么一個(gè)并發(fā)的版本還是要快呢? 答案其實(shí)是makeThumbnails在它還沒(méi)有完成工作之前就已經(jīng)返回了。 它啟動(dòng)了所有的goroutine, 沒(méi)一個(gè)文件名對(duì)應(yīng)一個(gè), 但沒(méi)有等待它們一直到執(zhí)行完畢。
沒(méi)有什么直接的辦法能夠等待goroutine完成, 但是我們可以改變goroutine里的代碼讓其能夠?qū)⑼瓿汕闆r報(bào)告給外部的goroutine知曉, 使用的方式是向一個(gè)共享的channel中發(fā)送事件。 因?yàn)槲覀円呀?jīng)知道內(nèi)部的goroutine只有l(wèi)en(filenames), 所以外部的goroutine只需要在返回之前對(duì)這些事件計(jì)數(shù)。
注意我們將f的值作為一個(gè)顯式的變量傳給了函數(shù), 而不是在循環(huán)的閉包中聲明:
回憶一下之前在5.6.1節(jié)中, 匿名函數(shù)中的循環(huán)變量快照問(wèn)題。 上面這個(gè)單獨(dú)的變量f是被所有的匿名函數(shù)值所共享, 且會(huì)被連續(xù)的循環(huán)迭代所更新的。 當(dāng)新的goroutine開(kāi)始執(zhí)行字面函數(shù)時(shí), for循環(huán)可能已經(jīng)更新了f并且開(kāi)始了另一輪的迭代或者(更有可能的)已經(jīng)結(jié)束了整個(gè)循環(huán), 所以當(dāng)這些goroutine開(kāi)始讀取f的值時(shí), 它們所看到的值已經(jīng)是slice的最后一個(gè)元素了。顯式地添加這個(gè)參數(shù), 我們能夠確保使用的f是當(dāng)go語(yǔ)句執(zhí)行時(shí)的“當(dāng)前”那個(gè)f。
如果我們想要從每一個(gè)worker goroutine往主goroutine中返回值時(shí)該怎么辦呢? 當(dāng)我們調(diào)用thumbnail.ImageFile創(chuàng)建文件失敗的時(shí)候, 它會(huì)返回一個(gè)錯(cuò)誤。 下一個(gè)版本的makeThumbnails會(huì)返回其在做拉伸操作時(shí)接收到的第一個(gè)錯(cuò)誤:
這個(gè)程序有一個(gè)微秒的bug。 當(dāng)它遇到第一個(gè)非nil的error時(shí)會(huì)直接將error返回到調(diào)用方, 使得沒(méi)有一個(gè)goroutine去排空errors channel。 這樣剩下的worker goroutine在向這個(gè)channel中發(fā)送值時(shí), 都會(huì)永遠(yuǎn)地阻塞下去, 并且永遠(yuǎn)都不會(huì)退出。 這種情況叫做goroutine泄露(§8.4.4),可能會(huì)導(dǎo)致整個(gè)程序卡住或者跑出out of memory的錯(cuò)誤。
最簡(jiǎn)單的解決辦法就是用一個(gè)具有合適大小的buffered channel, 這樣這些worker goroutine向channel中發(fā)送測(cè)向時(shí)就不會(huì)被阻塞。 (一個(gè)可選的解決辦法是創(chuàng)建一個(gè)另外的goroutine, 當(dāng)main goroutine返回第一個(gè)錯(cuò)誤的同時(shí)去排空channel)
下一個(gè)版本的makeThumbnails使用了一個(gè)buffered channel來(lái)返回生成的圖片文件的名字,附帶生成時(shí)的錯(cuò)誤。
我們最后一個(gè)版本的makeThumbnails返回了新文件們的大小總計(jì)數(shù)(bytes)。 和前面的版本都不一樣的一點(diǎn)是我們?cè)谶@個(gè)版本里沒(méi)有把文件名放在slice里, 而是通過(guò)一個(gè)string的channel傳過(guò)來(lái), 所以我們無(wú)法對(duì)循環(huán)的次數(shù)進(jìn)行預(yù)測(cè)。
為了知道最后一個(gè)goroutine什么時(shí)候結(jié)束(最后一個(gè)結(jié)束并不一定是最后一個(gè)開(kāi)始), 我們需要一個(gè)遞增的計(jì)數(shù)器, 在每一個(gè)goroutine啟動(dòng)時(shí)加一, 在goroutine退出時(shí)減一。 這需要一種特殊的計(jì)數(shù)器, 這個(gè)計(jì)數(shù)器需要在多個(gè)goroutine操作時(shí)做到安全并且提供提供在其減為零之前一直等待的一種方法。 這種計(jì)數(shù)類型被稱為sync.WaitGroup, 下面的代碼就用到了這種方法:
func makeThumbnails6 ( filenames <- chan string ) int64 { sizes := make ( chan int64 ) var wg sync. WaitGroupfor f := range filenames { wg. Add ( 1 ) go func ( f string ) { defer wg. Done ( ) thumb, err := thumbnail. ImageFile ( f) if err != nil { log. Println ( err) return } info, _ := os. Stat ( thumb) sizes <- info. Size ( ) } ( f) } go func ( ) { wg. Wait ( ) close ( sizes) } ( ) var total int64 for size := range sizes { total += size} return total
} 注意Add和Done方法的不對(duì)稱。 Add是為計(jì)數(shù)器加一, 必須在worker goroutine開(kāi)始之前調(diào)用, 而不是在goroutine中; 否則的話我們沒(méi)辦法確定Add是在"closer" goroutine調(diào)用Wait之前被調(diào)用。 并且Add還有一個(gè)參數(shù), 但Done卻沒(méi)有任何參數(shù); 其實(shí)它和Add(-1)是等價(jià)的。 我們使用defer來(lái)確保計(jì)數(shù)器即使是在出錯(cuò)的情況下依然能夠正確地被減掉。 上面的程序代碼結(jié)構(gòu)是當(dāng)我們使用并發(fā)循環(huán), 但又不知道迭代次數(shù)時(shí)很通常而且很地道的寫(xiě)法。
sizes channel攜帶了每一個(gè)文件的大小到main goroutine, 在main goroutine中使用了rangeloop來(lái)計(jì)算總和。 觀察一下我們是怎樣創(chuàng)建一個(gè)closer goroutine, 并讓其等待worker們?cè)陉P(guān)閉掉sizes channel之前退出的。 兩步操作: wait和close, 必須是基于sizes的循環(huán)的并發(fā)。 考慮一下另一種方案: 如果等待操作被放在了main goroutine中, 在循環(huán)之前, 這樣的話就永遠(yuǎn)都不會(huì)結(jié)束了, 如果在循環(huán)之后, 那么又變成了不可達(dá)的部分, 因?yàn)闆](méi)有任何東西去關(guān)閉這個(gè)channel, 這個(gè)循環(huán)就永遠(yuǎn)都不會(huì)終止。
圖8.5 表明了makethumbnails6函數(shù)中事件的序列。 縱列表示goroutine。 窄線段代表sleep,粗線段代表活動(dòng)。 斜線箭頭代表用來(lái)同步兩個(gè)goroutine的事件。 時(shí)間向下流動(dòng)。 注意maingoroutine是如何大部分的時(shí)間被喚醒執(zhí)行其range循環(huán), 等待worker發(fā)送值或者closer來(lái)關(guān)閉channel的。
6. 示例: 并發(fā)的Web爬蟲(chóng)
在5.6節(jié)中, 我們做了一個(gè)簡(jiǎn)單的web爬蟲(chóng), 用bfs(廣度優(yōu)先)算法來(lái)抓取整個(gè)網(wǎng)站。 在本節(jié)中, 我們會(huì)讓這個(gè)這個(gè)爬蟲(chóng)并行化, 這樣每一個(gè)彼此獨(dú)立的抓取命令可以并行進(jìn)行IO, 最大化利用網(wǎng)絡(luò)資源。 crawl函數(shù)和gopl.io/ch5/findlinks3中的是一樣的。 gopl.io/ch8/crawl1 主函數(shù)和5.6節(jié)中的breadthFirst(深度優(yōu)先)類似。 像之前一樣, 一個(gè)worklist是一個(gè)記錄了需要處理的元素的隊(duì)列, 每一個(gè)元素都是一個(gè)需要抓取的URL列表, 不過(guò)這一次我們用channel代替slice來(lái)做這個(gè)隊(duì)列。 每一個(gè)對(duì)crawl的調(diào)用都會(huì)在他們自己的goroutine中進(jìn)行并且會(huì)把他們抓到的鏈接發(fā)送回worklist。 注意這里的crawl所在的goroutine會(huì)將link作為一個(gè)顯式的參數(shù)傳入, 來(lái)避免“循環(huán)變量快照”的問(wèn)題(在5.6.1中有講解)。 另外注意這里將命令行參數(shù)傳入worklist也是在一個(gè)另外的goroutine中進(jìn)行的, 這是為了避免在main goroutine和crawler goroutine中同時(shí)向另一個(gè)goroutine通過(guò)channel發(fā)送內(nèi)容時(shí)發(fā)生死鎖(因?yàn)榱硪贿叺慕邮詹僮鬟€沒(méi)有準(zhǔn)備好)。 當(dāng)然, 這里我們也可以用buffered channel來(lái)解決問(wèn)題, 這里不再贅述。 現(xiàn)在爬蟲(chóng)可以高并發(fā)地運(yùn)行起來(lái), 并且可以產(chǎn)生一大坨的URL了, 不過(guò)還是會(huì)有倆問(wèn)題。 一個(gè)問(wèn)題是在運(yùn)行一段時(shí)間后可能會(huì)出現(xiàn)在log的錯(cuò)誤信息里的: 最初的錯(cuò)誤信息是一個(gè)讓人莫名的DNS查找失敗, 即使這個(gè)域名是完全可靠的。 而隨后的錯(cuò)誤信息揭示了原因: 這個(gè)程序一次性創(chuàng)建了太多網(wǎng)絡(luò)連接, 超過(guò)了每一個(gè)進(jìn)程的打開(kāi)文件數(shù)限制, 既而導(dǎo)致了在調(diào)用net.Dial像DNS查找失敗這樣的問(wèn)題 這個(gè)程序?qū)嵲谑翘麐尣⑿辛恕?無(wú)窮無(wú)盡地并行化并不是什么好事情, 因?yàn)椴还茉趺凑f(shuō), 你的系統(tǒng)總是會(huì)有一個(gè)些限制因素, 比如CPU核心數(shù)會(huì)限制你的計(jì)算負(fù)載, 比如你的硬盤(pán)轉(zhuǎn)軸和磁頭數(shù)限制了你的本地磁盤(pán)IO操作頻率, 比如你的網(wǎng)絡(luò)帶寬限制了你的下載速度上限, 或者是你的一個(gè)web服務(wù)的服務(wù)容量上限等等。 為了解決這個(gè)問(wèn)題, 我們可以限制并發(fā)程序所使用的資源來(lái)使之適應(yīng)自己的運(yùn)行環(huán)境。 對(duì)于我們的例子來(lái)說(shuō), 最簡(jiǎn)單的方法就是限制對(duì)links.Extract在同一時(shí)間最多不會(huì)有超過(guò)n次調(diào)用, 這里的n是fd的limit-20, 一般情況下。 這個(gè)一個(gè)夜店里限制客人數(shù)目是一個(gè)道理, 只有當(dāng)有客人離開(kāi)時(shí), 才會(huì)允許新的客人進(jìn)入店內(nèi)(譯注: 作者你個(gè)老流氓)。 我們可以用一個(gè)有容量限制的buffered channel來(lái)控制并發(fā), 這類似于操作系統(tǒng)里的計(jì)數(shù)信號(hào)量概念。 從概念上講, channel里的n個(gè)空槽代表n個(gè)可以處理內(nèi)容的token(通行證), 從channel里接收一個(gè)值會(huì)釋放其中的一個(gè)token, 并且生成一個(gè)新的空槽位。 這樣保證了在沒(méi)有接收介入時(shí)最多有n個(gè)發(fā)送操作。 (這里可能我們拿channel里填充的槽來(lái)做token更直觀一些, 不過(guò)還是這樣吧~)。 由于channel里的元素類型并不重要, 我們用一個(gè)零值的struct{}來(lái)作為其元素。 讓我們重寫(xiě)crawl函數(shù), 將對(duì)links.Extract的調(diào)用操作用獲取、 釋放token的操作包裹起來(lái), 來(lái)確保同一時(shí)間對(duì)其只有20個(gè)調(diào)用。 信號(hào)量數(shù)量和其能操作的IO資源數(shù)量應(yīng)保持接近。 gopl.io/ch8/crawl2 第二個(gè)問(wèn)題是這個(gè)程序永遠(yuǎn)都不會(huì)終止, 即使它已經(jīng)爬到了所有初始鏈接衍生出的鏈接。 (當(dāng)然, 除非你慎重地選擇了合適的初始化URL或者已經(jīng)實(shí)現(xiàn)了練習(xí)8.6中的深度限制, 你應(yīng)該還沒(méi)有意識(shí)到這個(gè)問(wèn)題)。 為了使這個(gè)程序能夠終止, 我們需要在worklist為空或者沒(méi)有crawl的goroutine在運(yùn)行時(shí)退出主循環(huán)。 這個(gè)版本中, 計(jì)算器n對(duì)worklist的發(fā)送操作數(shù)量進(jìn)行了限制。 每一次我們發(fā)現(xiàn)有元素需要被發(fā)送到worklist時(shí), 我們都會(huì)對(duì)n進(jìn)行++操作, 在向worklist中發(fā)送初始的命令行參數(shù)之前, 我們也進(jìn)行過(guò)一次++操作。 這里的操作++是在每啟動(dòng)一個(gè)crawler的goroutine之前。 主循環(huán)會(huì)在n 減為0時(shí)終止, 這時(shí)候說(shuō)明沒(méi)活可干了。 現(xiàn)在這個(gè)并發(fā)爬蟲(chóng)會(huì)比5.6節(jié)中的深度優(yōu)先搜索版快上20倍, 而且不會(huì)出什么錯(cuò), 并且在其完成任務(wù)時(shí)也會(huì)正確地終止。 下面的程序是避免過(guò)度并發(fā)的另一種思路。 這個(gè)版本使用了原來(lái)的crawl函數(shù), 但沒(méi)有使用計(jì)數(shù)信號(hào)量, 取而代之用了20個(gè)長(zhǎng)活的crawler goroutine, 這樣來(lái)保證最多20個(gè)HTTP請(qǐng)求在并發(fā)。 所有的爬蟲(chóng)goroutine現(xiàn)在都是被同一個(gè)channel-unseenLinks喂飽的了。 主goroutine負(fù)責(zé)拆分它從worklist里拿到的元素, 然后把沒(méi)有抓過(guò)的經(jīng)由unseenLinks channel發(fā)送給一個(gè)爬蟲(chóng)的goroutine。 seen這個(gè)map被限定在main goroutine中; 也就是說(shuō)這個(gè)map只能在main goroutine中進(jìn)行訪問(wèn)。 類似于其它的信息隱藏方式, 這樣的約束可以讓我們從一定程度上保證程序的正確性。例如, 內(nèi)部變量不能夠在函數(shù)外部被訪問(wèn)到; 變量(§2.3.4)在沒(méi)有被轉(zhuǎn)義的情況下是無(wú)法在函數(shù)外部訪問(wèn)的; 一個(gè)對(duì)象的封裝字段無(wú)法被該對(duì)象的方法以外的方法訪問(wèn)到。 在所有的情況下, 信息隱藏都可以幫助我們約束我們的程序, 使其不發(fā)生意料之外的情況。 crawl函數(shù)爬到的鏈接在一個(gè)專有的goroutine中被發(fā)送到worklist中來(lái)避免死鎖。 為了節(jié)省空間, 這個(gè)例子的終止問(wèn)題我們先不進(jìn)行詳細(xì)闡述了
7. 基于select的多路復(fù)用
下面的程序會(huì)進(jìn)行火箭發(fā)射的倒計(jì)時(shí)。 time.Tick函數(shù)返回一個(gè)channel, 程序會(huì)周期性地像一個(gè)節(jié)拍器一樣向這個(gè)channel發(fā)送事件。 每一個(gè)事件的值是一個(gè)時(shí)間戳, 不過(guò)更有意思的是其傳送方式。 gopl.io/ch8/countdown1 現(xiàn)在我們讓這個(gè)程序支持在倒計(jì)時(shí)中, 用戶按下return鍵時(shí)直接中斷發(fā)射流程。 首先, 我們啟動(dòng)一個(gè)goroutine, 這個(gè)goroutine會(huì)嘗試從標(biāo)準(zhǔn)輸入中調(diào)入一個(gè)單獨(dú)的byte并且, 如果成功了, 會(huì)向名為abort的channel發(fā)送一個(gè)值。 gopl.io/ch8/countdown2 現(xiàn)在每一次計(jì)數(shù)循環(huán)的迭代都需要等待兩個(gè)channel中的其中一個(gè)返回事件了: ticker channel當(dāng)一切正常時(shí)(就像NASA jorgon的"nominal", 譯注: 這梗估計(jì)我們是不懂了)或者異常時(shí)返回的abort事件。 我們無(wú)法做到從每一個(gè)channel中接收信息, 如果我們這么做的話, 如果第一個(gè)channel中沒(méi)有事件發(fā)過(guò)來(lái)那么程序就會(huì)立刻被阻塞, 這樣我們就無(wú)法收到第二個(gè)channel中發(fā)過(guò)來(lái)的事件。 這時(shí)候我們需要多路復(fù)用(multiplex)這些操作了, 為了能夠多路復(fù)用, 我們使用了select語(yǔ)句。 上面是select語(yǔ)句的一般形式。 和switch語(yǔ)句稍微有點(diǎn)相似, 也會(huì)有幾個(gè)case和最后的default選擇支。 每一個(gè)case代表一個(gè)通信操作(在某個(gè)channel上進(jìn)行發(fā)送或者接收)并且會(huì)包含一些語(yǔ)句組成的一個(gè)語(yǔ)句塊。 一個(gè)接收表達(dá)式可能只包含接收表達(dá)式自身(譯注: 不把接收到的值賦值給變量什么的), 就像上面的第一個(gè)case, 或者包含在一個(gè)簡(jiǎn)短的變量聲明中, 像第二個(gè)case里一樣; 第二種形式讓你能夠引用接收到的值。 select會(huì)等待case中有能夠執(zhí)行的case時(shí)去執(zhí)行。 當(dāng)條件滿足時(shí), select才會(huì)去通信并執(zhí)行case之后的語(yǔ)句; 這時(shí)候其它通信是不會(huì)執(zhí)行的。 一個(gè)沒(méi)有任何case的select語(yǔ)句寫(xiě)作select{}, 會(huì)永遠(yuǎn)地等待下去。 讓我們回到我們的火箭發(fā)射程序。 time.After函數(shù)會(huì)立即返回一個(gè)channel, 并起一個(gè)新的goroutine在經(jīng)過(guò)特定的時(shí)間后向該channel發(fā)送一個(gè)獨(dú)立的值。 下面的select語(yǔ)句會(huì)會(huì)一直等待到兩個(gè)事件中的一個(gè)到達(dá), 無(wú)論是abort事件或者一個(gè)10秒經(jīng)過(guò)的事件。 如果10秒經(jīng)過(guò)了還沒(méi)有abort事件進(jìn)入, 那么火箭就會(huì)發(fā)射。 下面這個(gè)例子更微秒。 ch這個(gè)channel的buffer大小是1, 所以會(huì)交替的為空或?yàn)闈M, 所以只有一個(gè)case可以進(jìn)行下去, 無(wú)論i是奇數(shù)或者偶數(shù), 它都會(huì)打印0 2 4 6 8。 如果多個(gè)case同時(shí)就緒時(shí), select會(huì)隨機(jī)地選擇一個(gè)執(zhí)行, 這樣來(lái)保證每一個(gè)channel都有平等的被select的機(jī)會(huì)。 增加前一個(gè)例子的buffer大小會(huì)使其輸出變得不確定, 因?yàn)楫?dāng)buffer既不為滿也不為空時(shí), select語(yǔ)句的執(zhí)行情況就像是拋硬幣的行為一樣是隨機(jī)的。 gopl.io/ch8/countdown3 time.Tick函數(shù)表現(xiàn)得好像它創(chuàng)建了一個(gè)在循環(huán)中調(diào)用time.Sleep的goroutine, 每次被喚醒時(shí)發(fā)送一個(gè)事件。 當(dāng)countdown函數(shù)返回時(shí), 它會(huì)停止從tick中接收事件, 但是ticker這個(gè)goroutine還依然存活, 繼續(xù)徒勞地嘗試從channel中發(fā)送值, 然而這時(shí)候已經(jīng)沒(méi)有其它的goroutine會(huì)從該channel中接收值了–這被稱為goroutine泄露(§8.4.4)。 Tick函數(shù)挺方便, 但是只有當(dāng)程序整個(gè)生命周期都需要這個(gè)時(shí)間時(shí)我們使用它才比較合適。 否則的話, 我們應(yīng)該使用下面的這種模式: 有時(shí)候我們希望能夠從channel中發(fā)送或者接收值, 并避免因?yàn)榘l(fā)送或者接收導(dǎo)致的阻塞, 尤其是當(dāng)channel沒(méi)有準(zhǔn)備好寫(xiě)或者讀時(shí)。 select語(yǔ)句就可以實(shí)現(xiàn)這樣的功能。 select會(huì)有一個(gè)default來(lái)設(shè)置當(dāng)其它的操作都不能夠馬上被處理時(shí)程序需要執(zhí)行哪些邏輯。 下面的select語(yǔ)句會(huì)在abort channel中有值時(shí), 從其中接收值; 無(wú)值時(shí)什么都不做。 這是一個(gè)非阻塞的接收操作; 反復(fù)地做這樣的操作叫做“輪詢channel”。 channel的零值是nil。 也許會(huì)讓你覺(jué)得比較奇怪, nil的channel有時(shí)候也是有一些用處的。 因?yàn)閷?duì)一個(gè)nil的channel發(fā)送和接收操作會(huì)永遠(yuǎn)阻塞, 在select語(yǔ)句中操作nil的channel永遠(yuǎn)都不會(huì)被select到 這使得我們可以用nil來(lái)激活或者禁用case, 來(lái)達(dá)成處理其它輸入或輸出事件時(shí)超時(shí)和取消的邏輯。 我們會(huì)在下一節(jié)中看到一個(gè)例子。
8. 示例: 并發(fā)的字典遍歷
在本小節(jié)中, 我們會(huì)創(chuàng)建一個(gè)程序來(lái)生成指定目錄的硬盤(pán)使用情況報(bào)告, 這個(gè)程序和Unix里的du工具比較相似。 大多數(shù)工作用下面這個(gè)walkDir函數(shù)來(lái)完成, 這個(gè)函數(shù)使用dirents函數(shù)來(lái)枚舉一個(gè)目錄下的所有入口。 gopl.io/ch8/du1
ioutil.ReadDir函數(shù)會(huì)返回一個(gè)os.FileInfo類型的slice, os.FileInfo類型也是os.Stat這個(gè)函數(shù)的返回值。 對(duì)每一個(gè)子目錄而言, walkDir會(huì)遞歸地調(diào)用其自身, 并且會(huì)對(duì)每一個(gè)文件也遞歸調(diào)用。 walkDir函數(shù)會(huì)向fileSizes這個(gè)channel發(fā)送一條消息。 這條消息包含了文件的字節(jié)大小。
下面的主函數(shù), 用了兩個(gè)goroutine。 后臺(tái)的goroutine調(diào)用walkDir來(lái)遍歷命令行給出的每一個(gè)路徑并最終關(guān)閉fileSizes這個(gè)channel。 主goroutine會(huì)對(duì)其從channel中接收到的文件大小進(jìn)行累加, 并輸出其和。
import ( "flag" "fmt" "io/ioutil" "os" "path/filepath"
) func main ( ) { flag. Parse ( ) roots := flag. Args ( ) if len ( roots) == 0 { roots = [ ] string { "." } } fileSizes := make ( chan int64 ) go func ( ) { for _ , root := range roots { walkDir ( root, fileSizes) } close ( fileSizes) } ( ) var nfiles, nbytes int64 for size := range fileSizes { nfiles++ nbytes += size} printDiskUsage ( nfiles, nbytes)
}
這個(gè)程序會(huì)在打印其結(jié)果之前卡住很長(zhǎng)時(shí)間。
如果在運(yùn)行的時(shí)候能夠讓我們知道處理進(jìn)度的話想必更好。 但是, 如果簡(jiǎn)單地把printDiskUsage函數(shù)調(diào)用移動(dòng)到循環(huán)里會(huì)導(dǎo)致其打印出成百上千的輸出。
下面這個(gè)du的變種會(huì)間歇打印內(nèi)容, 不過(guò)只有在調(diào)用時(shí)提供了-v的flag才會(huì)顯示程序進(jìn)度信息。 在roots目錄上循環(huán)的后臺(tái)goroutine在這里保持不變。 主goroutine現(xiàn)在使用了計(jì)時(shí)器來(lái)每500ms生成事件, 然后用select語(yǔ)句來(lái)等待文件大小的消息來(lái)更新總大小數(shù)據(jù), 或者一個(gè)計(jì)時(shí)器的事件來(lái)打印當(dāng)前的總大小數(shù)據(jù)。 如果-v的flag在運(yùn)行時(shí)沒(méi)有傳入的話, tick這個(gè)channel會(huì)保持為nil, 這樣在select里的case也就相當(dāng)于被禁用了。
gopl.io/ch8/du2
由于我們的程序不再使用range循環(huán), 第一個(gè)select的case必須顯式地判斷fileSizes的channel是不是已經(jīng)被關(guān)閉了, 這里可以用到channel接收的二值形式。 如果channel已經(jīng)被關(guān)閉了的話, 程序會(huì)直接退出循環(huán)。 這里的break語(yǔ)句用到了標(biāo)簽break, 這樣可以同時(shí)終結(jié)select和for兩個(gè)循環(huán); 如果沒(méi)有用標(biāo)簽就break的話只會(huì)退出內(nèi)層的select循環(huán), 而外層的for循環(huán)會(huì)使之進(jìn)入下一輪select循環(huán)。
現(xiàn)在程序會(huì)悠閑地為我們打印更新流:
然而這個(gè)程序還是會(huì)花上很長(zhǎng)時(shí)間才會(huì)結(jié)束。 無(wú)法對(duì)walkDir做并行化處理沒(méi)什么別的原因,無(wú)非是因?yàn)榇疟P(pán)系統(tǒng)并行限制。 下面這個(gè)第三個(gè)版本的du, 會(huì)對(duì)每一個(gè)walkDir的調(diào)用創(chuàng)建一個(gè)新的goroutine。 它使用sync.WaitGroup (§8.5)來(lái)對(duì)仍舊活躍的walkDir調(diào)用進(jìn)行計(jì)數(shù), 另一個(gè)goroutine會(huì)在計(jì)數(shù)器減為零的時(shí)候?qū)ileSizes這個(gè)channel關(guān)閉。
gopl.io/ch8/du3
由于這個(gè)程序在高峰期會(huì)創(chuàng)建成百上千的goroutine, 我們需要修改dirents函數(shù), 用計(jì)數(shù)信號(hào)量來(lái)阻止他同時(shí)打開(kāi)太多的文件, 就像我們?cè)?.7節(jié)中的并發(fā)爬蟲(chóng)一樣:
這個(gè)版本比之前那個(gè)快了好幾倍, 盡管其具體效率還是和你的運(yùn)行環(huán)境, 機(jī)器配置相關(guān)。
9. 并發(fā)的退出
有時(shí)候我們需要通知goroutine停止它正在干的事情, 比如一個(gè)正在執(zhí)行計(jì)算的web服務(wù), 然而它的客戶端已經(jīng)斷開(kāi)了和服務(wù)端的連接。 Go語(yǔ)言并沒(méi)有提供在一個(gè)goroutine中終止另一個(gè)goroutine的方法, 由于這樣會(huì)導(dǎo)致goroutine之間的共享變量落在未定義的狀態(tài)上。 在8.7節(jié)中的rocket launch程序中, 我們往名字叫abort的channel里發(fā)送了一個(gè)簡(jiǎn)單的值, 在countdown的goroutine中會(huì)把這個(gè)值理解為自己的退出信號(hào)。 但是如果我們想要退出兩個(gè)或者任意多個(gè)goroutine怎么辦呢? 一種可能的手段是向abort的channel里發(fā)送和goroutine數(shù)目一樣多的事件來(lái)退出它們。 如果這些goroutine中已經(jīng)有一些自己退出了, 那么會(huì)導(dǎo)致我們的channel里的事件數(shù)比goroutine還多, 這樣導(dǎo)致我們的發(fā)送直接被阻塞。 另一方面, 如果這些goroutine又生成了其它的goroutine, 我們的channel里的數(shù)目又太少了, 所以有些goroutine可能會(huì)無(wú)法接收到退出消息。 一般情況下我們是很難知道在某一個(gè)時(shí)刻具體有多少個(gè)goroutine在運(yùn)行著的。 另外, 當(dāng)一個(gè)goroutine從abort channel中接收到一個(gè)值的時(shí)候, 他會(huì)消費(fèi)掉這個(gè)值, 這樣其它的goroutine就沒(méi)法看到這條信息。 為了能夠達(dá)到我們退出goroutine的目的, 我們需要更靠譜的策略, 來(lái)通過(guò)一個(gè)channel把消息廣播出去, 這樣goroutine們能夠看到這條事件消息, 并且在事件完成之后, 可以知道這件事已經(jīng)發(fā)生過(guò)了。 只要一些小修改, 我們就可以把退出邏輯加入到前一節(jié)的du程序。 首先, 我們創(chuàng)建一個(gè)退出的channel, 這個(gè)channel不會(huì)向其中發(fā)送任何值, 但其所在的閉包內(nèi)要寫(xiě)明程序需要退出。我們同時(shí)還定義了一個(gè)工具函數(shù), cancelled, 這個(gè)函數(shù)在被調(diào)用的時(shí)候會(huì)輪詢退出狀態(tài)。 gopl.io/ch8/du4 下面我們創(chuàng)建一個(gè)從標(biāo)準(zhǔn)輸入流中讀取內(nèi)容的goroutine, 這是一個(gè)比較典型的連接到終端的程序。 每當(dāng)有輸入被讀到(比如用戶按了回車(chē)鍵), 這個(gè)goroutine就會(huì)把取消消息通過(guò)關(guān)閉done的channel廣播出去。 現(xiàn)在我們需要使我們的goroutine來(lái)對(duì)取消進(jìn)行響應(yīng)。 在main goroutine中, 我們添加了select的第三個(gè)case語(yǔ)句, 嘗試從done channel中接收內(nèi)容。 如果這個(gè)case被滿足的話, 在select到的時(shí)候即會(huì)返回, 但在結(jié)束之前我們需要把fileSizes channel中的內(nèi)容“排”空, 在channel被關(guān)閉之前, 舍棄掉所有值。 這樣可以保證對(duì)walkDir的調(diào)用不要被向fileSizes發(fā)送信息阻塞住,可以正確地完成。 walkDir這個(gè)goroutine一啟動(dòng)就會(huì)輪詢?nèi)∠麪顟B(tài), 如果取消狀態(tài)被設(shè)置的話會(huì)直接返回, 并且不做額外的事情。 這樣我們將所有在取消事件之后創(chuàng)建的goroutine改變?yōu)闊o(wú)操作。 在walkDir函數(shù)的循環(huán)中我們對(duì)取消狀態(tài)進(jìn)行輪詢可以帶來(lái)明顯的益處, 可以避免在取消事件發(fā)生時(shí)還去創(chuàng)建goroutine。 取消本身是有一些代價(jià)的; 想要快速的響應(yīng)需要對(duì)程序邏輯進(jìn)行侵入式的修改。 確保在取消發(fā)生之后不要有代價(jià)太大的操作可能會(huì)需要修改你代碼里的很多地方, 但是在一些重要的地方去檢查取消事件也確實(shí)能帶來(lái)很大的好處。 對(duì)這個(gè)程序的一個(gè)簡(jiǎn)單的性能分析可以揭示瓶頸在dirents函數(shù)中獲取一個(gè)信號(hào)量。 下面的select可以讓這種操作可以被取消, 并且可以將取消時(shí)的延遲從幾百毫秒降低到幾十毫秒。 現(xiàn)在當(dāng)取消發(fā)生時(shí), 所有后臺(tái)的goroutine都會(huì)迅速停止并且主函數(shù)會(huì)返回。 當(dāng)然, 當(dāng)主函數(shù)返回時(shí), 一個(gè)程序會(huì)退出, 而我們又無(wú)法在主函數(shù)退出的時(shí)候確認(rèn)其已經(jīng)釋放了所有的資源(譯注: 因?yàn)槌绦蚨纪顺隽?#xff0c; 你的代碼都沒(méi)法執(zhí)行了)。 這里有一個(gè)方便的竅門(mén)我們可以一用:取代掉直接從主函數(shù)返回, 我們調(diào)用一個(gè)panic, 然后runtime會(huì)把每一個(gè)goroutine的棧dump下來(lái)。 如果main goroutine是唯一一個(gè)剩下的goroutine的話, 他會(huì)清理掉自己的一切資源。 但是如果還有其它的goroutine沒(méi)有退出, 他們可能沒(méi)辦法被正確地取消掉, 也有可能被取消但是取消操作會(huì)很花時(shí)間; 所以這里的一個(gè)調(diào)研還是很有必要的。 我們用panic來(lái)獲取到足夠的信息來(lái)驗(yàn)證我們上面的判斷, 看看最終到底是什么樣的情況。
10. 示例: 聊天服務(wù)
我們用一個(gè)聊天服務(wù)器來(lái)終結(jié)本章節(jié)的內(nèi)容, 這個(gè)程序可以讓一些用戶通過(guò)服務(wù)器向其它所有用戶廣播文本消息。 這個(gè)程序中有四種goroutine。 main和broadcaster各自是一個(gè)goroutine實(shí)例, 每一個(gè)客戶端的連接都會(huì)有一個(gè)handleConn和clientWriter的goroutine。 broadcaster是select用法的不錯(cuò)的樣例, 因?yàn)樗枰幚砣N不同類型的消息。 下面演示的main goroutine的工作, 是listen和accept(譯注: 網(wǎng)絡(luò)編程里的概念)從客戶端過(guò)來(lái)的連接。 對(duì)每一個(gè)連接, 程序都會(huì)建立一個(gè)新的handleConn的goroutine, 就像我們?cè)诒菊麻_(kāi)頭的并發(fā)的echo服務(wù)器里所做的那樣。 gopl.io/ch8/chat 然后是broadcaster的goroutine。 他的內(nèi)部變量clients會(huì)記錄當(dāng)前建立連接的客戶端集合。 其記錄的內(nèi)容是每一個(gè)客戶端的消息發(fā)出channel的"資格"信息。 broadcaster監(jiān)聽(tīng)來(lái)自全局的entering和leaving的channel來(lái)獲知客戶端的到來(lái)和離開(kāi)事件。 當(dāng)其接收到其中的一個(gè)事件時(shí), 會(huì)更新clients集合, 當(dāng)該事件是離開(kāi)行為時(shí), 它會(huì)關(guān)閉客戶端的消息發(fā)出channel。 broadcaster也會(huì)監(jiān)聽(tīng)全局的消息channel, 所有的客戶端都會(huì)向這個(gè)channel中發(fā)送消息。 當(dāng)broadcaster接收到什么消息時(shí), 就會(huì)將其廣播至所有連接到服務(wù)端的客戶端。 現(xiàn)在讓我們看看每一個(gè)客戶端的goroutine。 handleConn函數(shù)會(huì)為它的客戶端創(chuàng)建一個(gè)消息發(fā)出channel并通過(guò)entering channel來(lái)通知客戶端的到來(lái)。 然后它會(huì)讀取客戶端發(fā)來(lái)的每一行文本, 并通過(guò)全局的消息channel來(lái)將這些文本發(fā)送出去, 并為每條消息帶上發(fā)送者的前綴來(lái)標(biāo)明消息身份。 當(dāng)客戶端發(fā)送完畢后, handleConn會(huì)通過(guò)leaving這個(gè)channel來(lái)通知客戶端的離開(kāi)并關(guān)閉連接。 現(xiàn)在讓我們看看每一個(gè)客戶端的goroutine。 handleConn函數(shù)會(huì)為它的客戶端創(chuàng)建一個(gè)消息發(fā)出channel并通過(guò)entering channel來(lái)通知客戶端的到來(lái)。 然后它會(huì)讀取客戶端發(fā)來(lái)的每一行文本, 并通過(guò)全局的消息channel來(lái)將這些文本發(fā)送出去, 并為每條消息帶上發(fā)送者的前綴來(lái)標(biāo)明消息身份。 當(dāng)客戶端發(fā)送完畢后, handleConn會(huì)通過(guò)leaving這個(gè)channel來(lái)通知客戶端的離開(kāi)并關(guān)閉連接。 另外, handleConn為每一個(gè)客戶端創(chuàng)建了一個(gè)clientWriter的goroutine來(lái)接收向客戶端發(fā)出消息channel中發(fā)送的廣播消息, 并將它們寫(xiě)入到客戶端的網(wǎng)絡(luò)連接。 客戶端的讀取方循環(huán)會(huì)在broadcaster接收到leaving通知并關(guān)閉了channel后終止。 下面演示的是當(dāng)服務(wù)器有兩個(gè)活動(dòng)的客戶端連接, 并且在兩個(gè)窗口中運(yùn)行的情況, 使用netcat來(lái)聊天: 當(dāng)與n個(gè)客戶端保持聊天session時(shí), 這個(gè)程序會(huì)有2n+2個(gè)并發(fā)的goroutine, 然而這個(gè)程序卻并不需要顯式的鎖(§9.2)。 clients這個(gè)map被限制在了一個(gè)獨(dú)立的goroutine中, broadcaster,所以它不能被并發(fā)地訪問(wèn)。 多個(gè)goroutine共享的變量只有這些channel和net.Conn的實(shí)例, 兩個(gè)東西都是并發(fā)安全的。 我們會(huì)在下一章中更多地解決約束, 并發(fā)安全以及goroutine中共享變量的含義。
總結(jié)
以上是生活随笔 為你收集整理的《Go语言圣经》学习笔记 第八章 Groroutines和Channels 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。