Go 語(yǔ)言 并發(fā)的退出

2023-03-14 16:57 更新

原文鏈接:https://gopl-zh.github.io/ch8/ch8-09.html


8.8. 并發(fā)的退出

有時(shí)候我們需要通知goroutine停止它正在干的事情,比如一個(gè)正在執(zhí)行計(jì)算的web服務(wù),然而它的客戶(hù)端已經(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ò)了。

回憶一下我們關(guān)閉了一個(gè)channel并且被消費(fèi)掉了所有已發(fā)送的值,操作channel之后的代碼可以立即被執(zhí)行,并且會(huì)產(chǎn)生零值。我們可以將這個(gè)機(jī)制擴(kuò)展一下,來(lái)作為我們的廣播機(jī)制:不要向channel發(fā)送值,而是用關(guān)閉一個(gè)channel來(lái)進(jìn)行廣播。

只要一些小修改,我們就可以把退出邏輯加入到前一節(jié)的du程序。首先,我們創(chuàng)建一個(gè)退出的channel,不需要向這個(gè)channel發(fā)送任何值,但其所在的閉包內(nèi)要寫(xiě)明程序需要退出。我們同時(shí)還定義了一個(gè)工具函數(shù),cancelled,這個(gè)函數(shù)在被調(diào)用的時(shí)候會(huì)輪詢(xún)退出狀態(tài)。

gopl.io/ch8/du4

var done = make(chan struct{})

func cancelled() bool {
    select {
    case <-done:
        return true
    default:
        return false
    }
}

下面我們創(chuàng)建一個(gè)從標(biāo)準(zhǔn)輸入流中讀取內(nèi)容的goroutine,這是一個(gè)比較典型的連接到終端的程序。每當(dāng)有輸入被讀到(比如用戶(hù)按了回車(chē)鍵),這個(gè)goroutine就會(huì)把取消消息通過(guò)關(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來(lái)對(duì)取消進(jìn)行響應(yīng)。在main goroutine中,我們添加了select的第三個(gè)case語(yǔ)句,嘗試從done channel中接收內(nèi)容。如果這個(gè)case被滿(mǎn)足的話(huà),在select到的時(shí)候即會(huì)返回,但在結(jié)束之前我們需要把fileSizes channel中的內(nèi)容“排”空,在channel被關(guān)閉之前,舍棄掉所有值。這樣可以保證對(duì)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這個(gè)goroutine一啟動(dòng)就會(huì)輪詢(xún)?nèi)∠麪顟B(tài),如果取消狀態(tài)被設(shè)置的話(huà)會(huì)直接返回,并且不做額外的事情。這樣我們將所有在取消事件之后創(chuàng)建的goroutine改變?yōu)闊o(wú)操作。

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)中我們對(duì)取消狀態(tài)進(jìn)行輪詢(xú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í)的延遲從幾百毫秒降低到幾十毫秒。

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ā)生時(shí),所有后臺(tái)的goroutine都會(huì)迅速停止并且主函數(shù)會(huì)返回。當(dāng)然,當(dāng)主函數(shù)返回時(shí),一個(gè)程序會(huì)退出,而我們又無(wú)法在主函數(shù)退出的時(shí)候確認(rèn)其已經(jīng)釋放了所有的資源(譯注:因?yàn)槌绦蚨纪顺隽?,你的代碼都沒(méi)法執(zhí)行了)。這里有一個(gè)方便的竅門(mén)我們可以一用:取代掉直接從主函數(shù)返回,我們調(diào)用一個(gè)panic,然后runtime會(huì)把每一個(gè)goroutine的棧dump下來(lái)。如果main goroutine是唯一一個(gè)剩下的goroutine的話(huà),他會(huì)清理掉自己的一切資源。但是如果還有其它的goroutine沒(méi)有退出,他們可能沒(méi)辦法被正確地取消掉,也有可能被取消但是取消操作會(huì)很花時(shí)間;所以這里的一個(gè)調(diào)研還是很有必要的。我們用panic來(lái)獲取到足夠的信息來(lái)驗(yàn)證我們上面的判斷,看看最終到底是什么樣的情況。

練習(xí) 8.10: HTTP請(qǐng)求可能會(huì)因http.Request結(jié)構(gòu)體中Cancel channel的關(guān)閉而取消。修改8.6節(jié)中的web crawler來(lái)支持取消http請(qǐng)求。(提示:http.Get并沒(méi)有提供方便地定制一個(gè)請(qǐng)求的方法。你可以用http.NewRequest來(lái)取而代之,設(shè)置它的Cancel字段,然后用http.DefaultClient.Do(req)來(lái)進(jìn)行這個(gè)http請(qǐng)求。)

練習(xí) 8.11: 緊接著8.4.4中的mirroredQuery流程,實(shí)現(xiàn)一個(gè)并發(fā)請(qǐng)求url的fetch的變種。當(dāng)?shù)谝粋€(gè)請(qǐng)求返回時(shí),直接取消其它的請(qǐng)求。



以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)