W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎勵
原文鏈接:https://gopl-zh.github.io/ch8/ch8-09.html
有時候我們需要通知goroutine停止它正在干的事情,比如一個正在執(zhí)行計算的web服務(wù),然而它的客戶端已經(jīng)斷開了和服務(wù)端的連接。
Go語言并沒有提供在一個goroutine中終止另一個goroutine的方法,由于這樣會導(dǎo)致goroutine之間的共享變量落在未定義的狀態(tài)上。在8.7節(jié)中的rocket launch程序中,我們往名字叫abort的channel里發(fā)送了一個簡單的值,在countdown的goroutine中會把這個值理解為自己的退出信號。但是如果我們想要退出兩個或者任意多個goroutine怎么辦呢?
一種可能的手段是向abort的channel里發(fā)送和goroutine數(shù)目一樣多的事件來退出它們。如果這些goroutine中已經(jīng)有一些自己退出了,那么會導(dǎo)致我們的channel里的事件數(shù)比goroutine還多,這樣導(dǎo)致我們的發(fā)送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel里的數(shù)目又太少了,所以有些goroutine可能會無法接收到退出消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運(yùn)行著的。另外,當(dāng)一個goroutine從abort
channel中接收到一個值的時候,他會消費(fèi)掉這個值,這樣其它的goroutine就沒法看到這條信息。為了能夠達(dá)到我們退出goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播出去,這樣goroutine們能夠看到這條事件消息,并且在事件完成之后,可以知道這件事已經(jīng)發(fā)生過了。
回憶一下我們關(guān)閉了一個channel并且被消費(fèi)掉了所有已發(fā)送的值,操作channel之后的代碼可以立即被執(zhí)行,并且會產(chǎn)生零值。我們可以將這個機(jī)制擴(kuò)展一下,來作為我們的廣播機(jī)制:不要向channel發(fā)送值,而是用關(guān)閉一個channel來進(jìn)行廣播。
只要一些小修改,我們就可以把退出邏輯加入到前一節(jié)的du程序。首先,我們創(chuàng)建一個退出的channel,不需要向這個channel發(fā)送任何值,但其所在的閉包內(nèi)要寫明程序需要退出。我們同時還定義了一個工具函數(shù),cancelled,這個函數(shù)在被調(diào)用的時候會輪詢退出狀態(tài)。
gopl.io/ch8/du4
var done = make(chan struct{})
func cancelled() bool {
select {
case <-done:
return true
default:
return false
}
}
下面我們創(chuàng)建一個從標(biāo)準(zhǔn)輸入流中讀取內(nèi)容的goroutine,這是一個比較典型的連接到終端的程序。每當(dāng)有輸入被讀到(比如用戶按了回車鍵),這個goroutine就會把取消消息通過關(guān)閉done的channel廣播出去。
// Cancel traversal when input is detected.
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
close(done)
}()
現(xiàn)在我們需要使我們的goroutine來對取消進(jìn)行響應(yīng)。在main goroutine中,我們添加了select的第三個case語句,嘗試從done channel中接收內(nèi)容。如果這個case被滿足的話,在select到的時候即會返回,但在結(jié)束之前我們需要把fileSizes channel中的內(nèi)容“排”空,在channel被關(guān)閉之前,舍棄掉所有值。這樣可以保證對walkDir的調(diào)用不要被向fileSizes發(fā)送信息阻塞住,可以正確地完成。
for {
select {
case <-done:
// Drain fileSizes to allow existing goroutines to finish.
for range fileSizes {
// Do nothing.
}
return
case size, ok := <-fileSizes:
// ...
}
}
walkDir這個goroutine一啟動就會輪詢?nèi)∠麪顟B(tài),如果取消狀態(tài)被設(shè)置的話會直接返回,并且不做額外的事情。這樣我們將所有在取消事件之后創(chuàng)建的goroutine改變?yōu)闊o操作。
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
defer n.Done()
if cancelled() {
return
}
for _, entry := range dirents(dir) {
// ...
}
}
在walkDir函數(shù)的循環(huán)中我們對取消狀態(tài)進(jìn)行輪詢可以帶來明顯的益處,可以避免在取消事件發(fā)生時還去創(chuàng)建goroutine。取消本身是有一些代價的;想要快速的響應(yīng)需要對程序邏輯進(jìn)行侵入式的修改。確保在取消發(fā)生之后不要有代價太大的操作可能會需要修改你代碼里的很多地方,但是在一些重要的地方去檢查取消事件也確實(shí)能帶來很大的好處。
對這個程序的一個簡單的性能分析可以揭示瓶頸在dirents函數(shù)中獲取一個信號量。下面的select可以讓這種操作可以被取消,并且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
func dirents(dir string) []os.FileInfo {
select {
case sema <- struct{}{}: // acquire token
case <-done:
return nil // cancelled
}
defer func() { <-sema }() // release token
// ...read directory...
}
現(xiàn)在當(dāng)取消發(fā)生時,所有后臺的goroutine都會迅速停止并且主函數(shù)會返回。當(dāng)然,當(dāng)主函數(shù)返回時,一個程序會退出,而我們又無法在主函數(shù)退出的時候確認(rèn)其已經(jīng)釋放了所有的資源(譯注:因?yàn)槌绦蚨纪顺隽耍愕拇a都沒法執(zhí)行了)。這里有一個方便的竅門我們可以一用:取代掉直接從主函數(shù)返回,我們調(diào)用一個panic,然后runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退出,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這里的一個調(diào)研還是很有必要的。我們用panic來獲取到足夠的信息來驗(yàn)證我們上面的判斷,看看最終到底是什么樣的情況。
練習(xí) 8.10: HTTP請求可能會因http.Request結(jié)構(gòu)體中Cancel channel的關(guān)閉而取消。修改8.6節(jié)中的web crawler來支持取消http請求。(提示:http.Get并沒有提供方便地定制一個請求的方法。你可以用http.NewRequest來取而代之,設(shè)置它的Cancel字段,然后用http.DefaultClient.Do(req)來進(jìn)行這個http請求。)
練習(xí) 8.11: 緊接著8.4.4中的mirroredQuery流程,實(shí)現(xiàn)一個并發(fā)請求url的fetch的變種。當(dāng)?shù)谝粋€請求返回時,直接取消其它的請求。
![]() | ![]() |
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: