在我的 Outcast(譯註:作者自己做的一款天氣預告 App) 資料服務器中,有幾個資料檢索任務要用到不同的 Go routine 來運行, 每個 routine 在設定的時間間隔內喚醒。 其中最複雜的工作是下載雷達圖像。 它複雜的原因在於:美國有 155 個雷達站,它們每 120 秒拍攝一張新照片, 我們要把所有的雷達圖像拼接在一起形成一張大的拼接圖。(譯註:有點像我們用手機拍攝全景片時,把多張邊緣有重疊的圖片拼接成一張大圖片) 當 go routine 被喚醒去下載新映像時,它必須儘快為所有 155 個網站都執行這個操作。 如果不夠及時的話,得到拼接圖將不同步,每個雷達站重疊的邊界部分會對不齊。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/Timer-Routines-And-Graceful-Shutdowns-In-Go/radar-img-1.png)左邊的雷達圖是坦帕灣雷達站在下午 4:51 拍攝的,你可以看到,這個雷達站覆蓋了佛羅里達州的大部分範圍,事實上,這個雷達站甚至涵蓋了其它雷達站的範圍,比如說邁阿密的。右邊的雷達圖是邁阿密雷達站在下午 4:53 拍攝的,跟右圖存在了兩分鐘的差異,(我把這種情況稱之為 glare)當我們把這兩個雷達圖鋪疊在地圖上的時候,你不會發現有什麼不對的地方,但是,如果這兩個圖片之前的延遲不止幾分鐘的時候,我們裸眼就能看出有很大的區別。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/Timer-Routines-And-Graceful-Shutdowns-In-Go/radar-img-2.png)藍色是雷達的噪點,我們會把它給過濾掉,所以我們剩下綠色、紅色和黃色的色塊來表示真正的天氣狀況。上面的圖片是在下午 4:46 下載並處理好的,你可以看到他們很接近,能夠很好的拼接在一起。我們的代碼的第一個實現中,使用了單個 go routine,每10分鐘喚醒一次,每次這個 go routine 喚醒,它都需要 3 到 4 分鐘時間下載、處理、儲存並把 155 個雷達站的資料寫入的到 mongo 裡面去。雖然我會把每個地區的圖片 儘可能地拼接起來,但是這些圖片存在的延遲差異實在是太大了。每個雷達站都存在一兩分鐘的延遲,所有的雷達站的延遲疊加起來,使這個問題凸顯出來。對於所有工作,我都會儘可能地使用單個 go routine 來實現,因為這樣能讓事情保持簡單。但在這個情況下,單一 go routine 並不能湊效。我必須同時處理多個雷達站的資料,來減少延遲造成的差異。在我添加了一個工作池來處理同時多個雷達站的資料後,我能夠在一分鐘之內把 155 個雷達站的資料都處理好了。目前為止,我還沒收到用戶端Team Dev的抱怨。在篇文章裡面,我們主要關註定時 routine 和退出的代碼。在下一個文章,我會告訴你怎麼去為你的項目添加一個工作池。我打算提供一個完整的可以啟動並執行例子。它也許可以作為一個參考模板來讓你實現你自己的代碼。要下載這個例子,你可以開啟一個新的終端會話,輸入下面的命令:```bashcd $HOMEexport GOPATH=$HOME/examplego get github.com/goinggo/timerdesignpatterncd example/bin./timerdesignpattern```Outcast 資料服務器是個單應用程式,它設計為長期啟動並執行服務程式,這種類型的程式很少會需要退出。讓你的程式能在需要的時候優雅地退出是很重要的。當我在開發這種類型的程式時,我總是要從開始就確保,我可以通過某些訊號通知應用程式退出,並且不會讓它掛起。一個程式,最糟糕的事情莫過於需要你強制殺死進程才能退出了。樣本程式建立了一個單一的 go routine 並且指定這個 routine 每 15 秒喚醒一次. 當 routine 喚醒的時候,它會進行一個大概耗時 10 秒的操作。當工作完成以後,它再計算需要睡多少秒,來確保這個 routine 能夠保持每 15 秒喚醒一次。讓我們試試運行這個程式並且在它啟動並執行時候把它退出掉。然後我們就可以開始學習它是怎麼實現的。我們可以在程式啟動並執行任何時候,按斷行符號鍵來退出這個程式。下面是程式運行 7 秒鐘後退出的輸出:```2013-09-04T18:58:45.505 : main : main : Starting Program2013-09-04T18:58:45.505 : main : workmanager.Startup : Started2013-09-04T18:58:45.505 : main : workmanager.Startup : Completed2013-09-04T18:58:45.505 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Started2013-09-04T18:58:45.505 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Info : Wait To Run : Seconds[15]2013-09-04T18:58:52.666 : main : workmanager.Shutdown : Started2013-09-04T18:58:52.666 : main : workmanager.Shutdown : Info : Shutting Down2013-09-04T18:58:52.666 : main : workmanager.Shutdown : Info : Shutting Down Work Timer2013-09-04T18:58:52.666 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Shutting Down2013-09-04T18:58:52.666 : main : workmanager.Shutdown : Completed2013-09-04T18:58:52.666 : main : main : Program Complete```這是一次很棒的初次測試,當我們指示程式退出的時候,它優雅地退出了。下一步我們試試看等它開始工作(譯註:這個程式運行後要等 15 秒才開始執行第一次的工作)之後再嘗試退出它。```2013-09-04T19:14:21.312 : main : main : Starting Program2013-09-04T19:14:21.312 : main : workmanager.Startup : Started2013-09-04T19:14:21.312 : main : workmanager.Startup : Completed2013-09-04T19:14:21.312 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Started2013-09-04T19:14:21.313 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Info : Wait To Run : Seconds[15]2013-09-04T19:14:36.313 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Woke Up2013-09-04T19:14:36.313 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Started2013-09-04T19:14:36.313 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Processing Images For Station : 02013-09-04T19:14:36.564 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Processing Images For Station : 12013-09-04T19:14:36.815 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Processing Images For Station : 22013-09-04T19:14:37.065 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Processing Images For Station : 32013-09-04T19:14:37.129 : main : workmanager.Shutdown : Started2013-09-04T19:14:37.129 : main : workmanager.Shutdown : Info : Shutting Down2013-09-04T19:14:37.129 : main : workmanager.Shutdown : Info : Shutting Down Work Timer2013-09-04T19:14:37.315 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Info : Request To Shutdown2013-09-04T19:14:37.315 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Info : Wait To Run : Seconds[14]2013-09-04T19:14:37.315 : WorkTimer : _WorkManager.GoRoutine_WorkTimer : Shutting Down2013-09-04T19:14:37.316 : main : workmanager.Shutdown : Completed2013-09-04T19:14:37.316 : main : main : Program Complete```這次我等了 15 秒,讓程式開始工作,當它開始工作並完成了第四個圖片的處理之後,我指示程式退出。它也及時停止了工作並優雅地退出了。我們來看看實現定時 routine 和優雅退出的核心代碼:```gofunc (wm *WorkManager) WorkTimer() { for { select { case <-wm.ShutdownChannel: wm.ShutdownChannel <- "Down" return case <-time.After(TimerPeriod): break } startTime := time.Now() wm.PerformTheWork() endTime := time.Now() duration := endTime.Sub(startTime) wait = TimerPeriod - duration }}```為了更加簡潔易讀,我把注釋和輸出日誌的代碼去掉了。這是一個經典的作業隊列 channel, 並且這個解決方案非常的優雅。比起用 C# 實現的同樣的東西,優雅多了。`WorkTimer()` 函數作為一個 go routine 運行:```gofunc Startup() { wm = WorkManager{ Shutdown: false, ShutdownChannel: make(chan string), } go wm.WorkTimer()}````WorkManager` 是以單例(譯註:設計模式的一種,參考[單例模式](https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F))的模式建立的,它建立完後就開始啟動定時 routine。它有一個 channel 負責關閉定時 routine,還有一個標誌用來指明系統是否正在退出。定時 routine 在內部有一個無限的迴圈,所以它不會終止,除非我們們指明要它退出。我們來看看這個迴圈裡面關於 channel 的部分:```goselect {case <-wm.ShutdownChannel: wm.ShutdownChannel <- "Down" returncase <-time.After(TimerPeriod): break}wm.PerformTheWork()```我們使用了 `select` 語句。這個語句在官方文檔的解釋在這裡:http://golang.org/ref/spec#Select_statements我們使用 `select` 語句來保證定時 routine 只有到了工作時間或者收到退出指令的時候才會被喚醒。`Select` 語句使得定時 routine 在所有通道都沒有收到訊號的時候阻塞。每次只有其中一個分支會執行,這讓我們的代碼保持同步。`select` 語句讓我們用簡潔的代碼在多個 channel 間實現原子的、“routine 安全”的操作(只要我們把這幾個 channel 都放在同一個 `select` 語句裡面)。在我們的定時 routine 的 `select` 語句裡面有兩個 channel,一個負責退出 routine,一個負責執行任務。退出定時 routine 的代碼如下:```gofunc Shutdown() { wm.Shutdown = true wm.ShutdownChannel <- "Down" <-wm.ShutdownChannel close(wm.ShutdownChannel)}```當需要退出的時候,我們把 `Shutdown` 標記置為 `true`,然後發送字串 `"Down"` 到 `ShutdownChannel`,然後我們從 `ShutdownChannel` 裡面等待來自 定時 routine 的回複。這種資料通訊同步了主程式和定時 routine 之間的整個退出過程。 非常的棒,簡單而優雅。要以一個固定的時間間隔喚醒定時 routine,我使用了一個叫做 `time.After` 的函數,這個函數等待一段指定的時間,然後把目前時間發送到指定的 channel 裡面。這又喚醒了 `select`,從而使得 `PerformTheWork` 函數得以執行。當 `PerformTheWork` 函數返回時,定時 routine 又再一次回到睡眠狀態,除非又有 channel 收到了新的訊號。我們來看一下 `PerformTheWork` 函數:```gofunc (wm *_WorkManager) PerformTheWork() { for count := 0; count < 40; count++ { if wm.Shutdown == true { return } fmt.Println("Processing Images For Station:", count) time.Sleep(time.Millisecond * 250) }}```這個函數每 250 微秒在控制台輸出一次資訊,一共輸出 40 次。這將會耗費大概 10 秒的時間來完成這個任務。在這個迴圈裡面,每次迭代都檢查一下 `Shutdown` 這個標記是否置為 `true`。這非常重要,因為它使得這個函數在程式退出時,能夠非常快的結束掉。我們不希望使用這個程式的管理者在退出這個程式的時候,覺得覺得這個程式被掛起了。當 `PerformTheWork` 函數結束後,定時 routine 得以再次執行 `select` 語句,如果程式正在退出的過程中,那麼 `select` 語句會立刻喚醒來處理來自 `ShutdownChannel` 的訊號。在這裡,定時 routine 再通知主 routine 它正在退出,從而整個程式得以優雅地退出。這就是我的定時 routine 和優雅退出程式的代碼模式,你也可以把這個模式應用在你的程式中。如果你從 GoingGo 的代碼倉庫下載了整個樣本的話,你可以看到實戰的代碼和一些小工具。閱讀下面的文章可以學習到怎麼實現一個能夠處理多個 go routine 的工作池,正如我上述的處理雷達圖像的那個工作池一樣:https://studygolang.com/articles/14481
via: https://www.ardanlabs.com/blog/2013/09/timer-routines-and-graceful-shutdowns.html
作者:William Kennedy 譯者:Alex-liutao 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
132 次點擊 ∙ 1 贊