使用 Goroutines 池來處理基於任務的操作

來源:互聯網
上載者:User
*作者註:使用 Go 語言工作了一段時間之後,我學會了如何使用無緩衝 channel 來構建 Goroutines 池,我喜歡這種方式勝於此文章中所展示的方式。話雖如此,此文章在它所描述的情境中依然有巨大價值。*我在多個場合都被問到為什麼使用工作池模式,為什麼不在需要的時候啟動所需要的 Goroutines?我的答案一直是:受限於工作的類型、你所擁有的計算資源和所處平台的限制,盲目地使用 Goroutines 將會導致程式運行緩慢,進而傷害整個系統的響應和效能。每個程式、系統和平台都有短板。不管是記憶體、CPU 或者頻寬資源也都不是無限的。因此對於我們的程式來說,減少資源消耗、重用有限資源是非常重要的。工作池恰好提供了這樣一種模式,可以協助程式管理資源,提供調節資源的選項。展示了工作池的原理:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/pool-go/1.png)如所示,主業務常式提交了100個任務到工作池中。工作池將它們都排入隊列,當一個 Goroutine 空閑,工作池從任務隊列中取出一個任務分配到此 Goroutine 上,此任務將會得到執行。執行完畢後此 Goroutine 將會再次空閑並等待處理其他任務。Goroutines 的數量和隊列的容量是可配置的,這意味著工作池可以用於程式的效能調節。Go 語言使用 Goroutine 替代了線程。Go 運行環境管理了一個內部的線程池並且在這個池內調度 Goroutines。線程池是最小化 Go 運行環境的負載和最大化程式效能的關鍵手段。當我們建立了一個新的 Goroutine 時,Go 運行環境將在內部線程池中管理和調度這個 Goroutine。這個原理就和作業系統在閒置 CPU 核心上調度線程一樣。通過 Goroutine 我們可以獲得同調度線程池一樣的效果,甚至可能更好。對於處理基於任務的操作我有一個簡單的原則:少即是多。我總是想要知道對於特定操作,最好的結果需要的 Goroutines 的最小值是多少。最好的結果不僅僅是全部的任務需要花費多長時間來完成,同樣還包括處理這些任務對程式、系統和平台所產生的影響。你必須同時考慮到短期影響和長期影響。在系統或程式負載較輕的情況下,我們很容易就能擷取到非常快的處理速度。但是某天系統負荷的輕微增加就會導致之前的配置不起作用,而我們並沒有意識到正是我們在嚴重傷害和我們互動的系統。我們可能把資料庫或者網路伺服器用的太狠了,最終造成了系統的宕機。突發的100個並發任務可以運行正常,但是持續一個小時的並發可能就是致命的。工作池並不是可以解決全世界運算問題的魔力仙女,它卻可以用在你的程式中處理基於任務的操作。它可以根據你的系統資料表現提供配置選項和控制功能。隨著系統變化,你也有足夠的靈活度來改變。現在讓我們舉個例子來證明在處理基於任務的操作方面工作池要比盲目的產生 Goroutines 更有效率。我們的測試程式運行某一個任務,它會擷取一個 MongoDB 的串連,在資料庫上執行查詢命令並返回資料。一般的業務中都會有類似的功能。這個測試程式將會提交100個任務到工作池中,運行5次後統計平均已耗用時間。開啟終端,運行如下的命令來下載代碼:```export GOPATH=$HOME/examplego get github.com/goinggo/workpooltestcd $HOME/example/bin```我們建立一個包含 100 個 Goroutines 的工作池,用它來類比盲目的根據任務數產生相同數量的 Goroutine 的模型。```./workpooltest 100 off```第一個參數告訴程式建立100個 Goroutines 的工作池,第二個參數告訴程式關閉詳細的日誌輸出。在我的 Macbook 上,運行上面這個命令的結果是:```CPU[8] Routines[100] AmountOfWork[100] Duration[4.599752] MaxRoutines[100] MaxQueued[3]CPU[8] Routines[100] AmountOfWork[100] Duration[5.799874] MaxRoutines[100] MaxQueued[3]CPU[8] Routines[100] AmountOfWork[100] Duration[5.325222] MaxRoutines[100] MaxQueued[3]CPU[8] Routines[100] AmountOfWork[100] Duration[4.652793] MaxRoutines[100] MaxQueued[3]CPU[8] Routines[100] AmountOfWork[100] Duration[4.552223] MaxRoutines[100] MaxQueued[3]Average[4.985973]```輸出結果中的參數含義:```CPU[8] : The number of cores on my machineRoutines[100] : The number of routines in the work poolAmountOfWork[100] : The number of tasks to runDuration[4.599752] : The amount of time in seconds the run tookMaxRoutines[100] : The max number of routines that were active during the runMaxQueued[3] : The max number of tasks waiting in queued during the run```現在讓我們運行 64 個 Goroutines 的工作池:```CPU[8] Routines[64] AmountOfWork[100] Duration[4.574367] MaxRoutines[64] MaxQueued[35]CPU[8] Routines[64] AmountOfWork[100] Duration[4.549339] MaxRoutines[64] MaxQueued[35]CPU[8] Routines[64] AmountOfWork[100] Duration[4.483110] MaxRoutines[64] MaxQueued[35]CPU[8] Routines[64] AmountOfWork[100] Duration[4.595183] MaxRoutines[64] MaxQueued[35]CPU[8] Routines[64] AmountOfWork[100] Duration[4.579676] MaxRoutines[64] MaxQueued[35]Average[4.556335]```接著是 24 個 Goroutines 的結果:```CPU[8] Routines[24] AmountOfWork[100] Duration[4.595832] MaxRoutines[24] MaxQueued[75]CPU[8] Routines[24] AmountOfWork[100] Duration[4.430000] MaxRoutines[24] MaxQueued[75]CPU[8] Routines[24] AmountOfWork[100] Duration[4.477544] MaxRoutines[24] MaxQueued[75]CPU[8] Routines[24] AmountOfWork[100] Duration[4.550768] MaxRoutines[24] MaxQueued[75]CPU[8] Routines[24] AmountOfWork[100] Duration[4.629989] MaxRoutines[24] MaxQueued[75]Average[4.536827]```最後是 8 個 Goroutines:```CPU[8] Routines[8] AmountOfWork[100] Duration[4.616843] MaxRoutines[8] MaxQueued[91]CPU[8] Routines[8] AmountOfWork[100] Duration[4.477796] MaxRoutines[8] MaxQueued[91]CPU[8] Routines[8] AmountOfWork[100] Duration[4.841476] MaxRoutines[8] MaxQueued[91]CPU[8] Routines[8] AmountOfWork[100] Duration[4.906065] MaxRoutines[8] MaxQueued[91]CPU[8] Routines[8] AmountOfWork[100] Duration[5.035139] MaxRoutines[8] MaxQueued[91]Average[4.775464]```讓我們收集一下這幾個運行結果:```100 Go Routines : 4.985973 :64 Go Routines : 4.556335 : ~430 Milliseconds Faster24 Go Routines : 4.536827 : ~450 Milliseconds Faster8 Go Routines : 4.775464 : ~210 Milliseconds Faster```上述測試結果告訴我們如果單核運行 3 個 Goroutines 將獲得最好的結果。3 似乎是個神奇的數字,這個配置在我寫的每個 Go 程式中都會產生很好的結果。如果我們啟動並執行程式擁有更多的核心,我們可以簡單地增加 Goroutines 的數量來充分利用更多的資源和能耗。這就意味著如果 MongoDB 可以處理多出來的串連,那麼我們總歸可以調整工作池的尺寸和容量來擷取最優結果。我們已經證明了對於特定的操作,每個任務都盲目的產生 Goroutines 並不是最好的解決方案。我們來看看工作池的代碼是怎麼工作的:工作池的代碼可以在你下載的代碼路徑中找到:```cd $HOME/example/src/github.com/goinggo/workpool```workpool.go 這個檔案中包含了所有的代碼。我移除了全部的注釋和部分程式碼使我們聚焦在重要的部分。我們首先看看構建工作池的類型:```gotype WorkPool struct { shutdownQueueChannel chan string shutdownWorkChannel chan struct{} shutdownWaitGroup sync.WaitGroup queueChannel chan poolWork workChannel chan PoolWorker queuedWork int32 activeRoutines int32 queueCapacity int32}type poolWork struct { Work PoolWorker ResultChannel chan error}type PoolWorker interface { DoWork(workRoutine int)}```WorkPool 是代表工作池的公用類型。它實現了兩個 channel。WorkChannel 處於工作池的核心位置,它管理著需要處理的工作隊列。所有 Goroutines 都會等待這個 channel 的訊號。QueueChannel 用於管理提交工作到 WorkChannel。QueueChannel 將工作是否進入隊列的確認提供給調用方,它同時負責維護 QueuedWork 和 QueuedCapacity 這兩個計數器。PoolWork 結構體定義了發送給 QueueChannel 用於處理進入隊列請求的資料。它包含了涉及到使用者 PoolWorker 對象的介面和一個接收任務已經進入隊列的確認的 channel。PoolWorker 的介面定義了 DoWork 函數,其中的一個參數代表了運行此任務的 Goroutines 的內部 id。此 id 對於記錄日誌和其他針對 Goroutines 層級的事務都很有協助。PoolWorker 介面是工作池中用於接收和運行任務的核心。讓我們看一個簡單的用戶端實現:```gotype MyTask struct { Name string WP *workpool.WorkPool}func (mt *MyTask) DoWork(workRoutine int) { fmt.Println(mt.Name) fmt.Printf("*******> WR: %d QW: %d AR: %d\n", workRoutine, mt.WP.QueuedWork(), mt.WP.ActiveRoutines()) time.Sleep(100 * time.Millisecond)}func main() { runtime.GOMAXPROCS(runtime.NumCPU()) workPool := workpool.New(runtime.NumCPU() * 3, 100) task := MyTask{ Name: "A" + strconv.Itoa(i), WP: workPool, } err := workPool.PostWork("main", &task) …}```我建立了一個 MyTask 的類型,它定義了工作執行的狀態。接著我實現一個 MyTask 的函數成員 DoWork,它同時符合 PoolWorker 介面的簽名。由於 MyTask 實現了 PoolWorker 的介面,MyTask 類型的對象也被認為是 PoolWorker 類型的對象。現在我們把 MyTask 類型的對象傳入 PostWork 方法中。要學習更多的 Go 語言中介面和基於對象編程,可以參考如下連結:https://www.ardanlabs.com/blog/2013/07/object-oriented-programming-in-go.html我設定 Go 運行環境使用我本機上的全部 CPU 和核心,我建立了一個 24 個 Goroutines 的工作池。我本機有 8 個核心,就像上面我們得到的結論,每個核心分配 3 個 Goroutines 是比較好的配置。最後一個參數是告訴工作池建立一個容量為 100 個任務的隊列。接著我建立了一個 MyTask 的對象並且提交到隊列中。為了記錄日誌,PostWork 方法的第一個參數可以設定成調用方的名稱。如果調用返回的 err 參數是空,表明此任務已經得到提交;如果非空,大機率意味著已經超過了隊列的容量,你的任務未能得到提交。我們到代碼內部看看 WorkPool 對象是如何被建立和啟動的:```gofunc New(numberOfRoutines int, queueCapacity int32) *WorkPool { workPool = WorkPool{ shutdownQueueChannel: make(chan string), shutdownWorkChannel: make(chan struct{}), queueChannel: make(chan poolWork), workChannel: make(chan PoolWorker, queueCapacity), queuedWork: 0, activeRoutines: 0, queueCapacity: queueCapacity, } for workRoutine := 0; workRoutine < numberOfRoutines; workRoutine++ { workPool.shutdownWaitGroup.Add(1) go workPool.workRoutine(workRoutine) } go workPool.queueRoutine() return &workPool}```我們看到在上面的用戶端範例程式碼中 Goroutines 的數量和隊列長度被傳入 New 函數。WorkChannel 是一個緩衝 channel,是用於儲存需要處理的工作的隊列。QueueChannel 是一個非緩衝 channel,用於同步對 WorkChannel 緩衝區的訪問並維護計數器。要學習更多關於緩衝和非緩衝 channel 的知識,請訪問此連結:http://golang.org/doc/effective_go.html#channels當 channel 初始化完畢後,我們就可以去建立 Goroutines 了。首先我們對每個 Goroutine 的 WaitGroup 加1來關閉它們。接著建立 Goroutines。最後開啟 QueueRoutine 來接收工作。要學習關閉Goroutines的代碼和WaitGroup是如何工作的,請閱讀此連結:http://dave.cheney.net/2013/04/30/curious-channels關閉工作池的實現如下所示:```gofunc (wp *WorkPool) Shutdown(goRoutine string) { wp.shutdownQueueChannel <- "Down" <-wp.sutdownQueueChannel close(wp.queueChannel) close(wp.shutdownQueueChannel) close(wp.shutdownWorkChannel) wp.shutdownWaitGroup.Wait() close(wp.workChannel)}```Shutdown函數首先關閉 QueueRoutine,這樣就不會接收更多的請求。接著關閉 ShutdownWorkChannel,並等待每個 Goroutine 去對 WaitGroup 計數器做減操作。一旦最後一個 Goroutine 調用了 Done 函數,等待函數 Wait 將會返回,工作池將會被關閉。現在讓我們看看 PostWork 和 QueueRoutine 函數:```gofunc (wp *WorkPool) PostWork(goRoutine string, work PoolWorker) (err error) { poolWork := poolWork{work, make(chan error)} defer close(poolWork.ResultChannel) wp.queueChannel <- poolWork return <-poolWork.ResultChannel}``````gofunc (wp *WorkPool) queueRoutine() { for { select { case <-wp.shutdownQueueChannel: wp.shutdownQueueChannel <- "Down" return case queueItem := <-wp.queuechannel: if atomic.AddInt32(&wp.queuedWork, 0) == wp.queueCapacity { queueItem.ResultChannel <- fmt.Errorf("Thread Pool At Capacity") continue } atomic.AddInt32(&wp.queuedWork, 1) wp.workChannel <- queueItem.Work queueItem.ResultChannel <- nil break } }}```PostWork 和 QueueRoutine 函數背後的思想是把對 WorkChannel 緩衝區的訪問序列化,保證隊列順序和維護計數器。當工作被提交到 channel 的時候,Go 運行環境保證它總是會被置於 WorkChannel 的末尾。當 QueueChannel 收到訊號,QueueRoutine 將會接收到一項工作。代碼先檢查隊列是否還有空位,如果有 PoolWorker 的對象將排入 WorkChannel 的緩衝區。當所有的事務都排入隊列後,調用方將獲得返回結果。我們來看一下 WorkRoutine 的函數:```gofunc (wp *WorkPool) workRoutine(workRoutine int) { for { select { case <-wp.shutdownworkchannel: wp.shutdownWaitGroup.Done() return case poolWorker := <-wp.workChannel: wp.safelyDoWork(workRoutine, poolWorker) break } }}``````gofunc (wp *WorkPool) safelyDoWork(workRoutine int, poolWorker PoolWorker) { defer catchPanic(nil, "workRoutine", "workpool.WorkPool", "SafelyDoWork") defer atomic.AddInt32(&wp.activeRoutines, -1) atomic.AddInt32(&wp.queuedWork, -1) atomic.AddInt32(&wp.activeRoutines, 1) poolWorker.DoWork(workRoutine)}```Go 運行環境通過向空閑中的 Goroutine 對應的 WorkChannel 發送訊號的方式給 Goroutine 分配工作。當 channel 接收到訊號,Go 運行環境將會把 channel 緩衝區的第一個任務傳給 Goroutine 來處理。這個 channel 的緩衝區就像是一個先入先出的隊列。如果全部的 Goroutines 都處於忙碌狀態,那所有的剩下的工作都要等待。只要一個 routine 完成它被分配的工作,它就會返回並繼續等待 WorkChannel 的通知。如果 channel 的緩衝區有工作,那 Go 運行環境將會喚醒這個 Goroutine。代碼使用了 SafelyDo 模式,因為代碼會調用處於使用者模式下的代碼,存在崩潰的可能,而你肯定不希望 Goroutine 跟著一起停止工作。注意第一個 defer 的聲明,它將會捕獲任何的崩潰,保持代碼的持續運行。其他部分的代碼會安全的增加或減少計數器,通過介面調用使用者模式下的部分。要學習更多捕獲崩潰的知識請閱讀如下的文章:https://www.ardanlabs.com/blog/2013/06/understanding-defer-panic-and-recover.html這就是代碼的核心以及它如何?了這樣的模式。WorkPool 優雅的展示了 channel 的使用。我可以使用很少量的代碼來處理工作。增加隊列的保證機制和計數器的維護都只是小菜一碟。請從 GoingGo 的代碼倉庫下載代碼並且親自試試吧。

via: https://www.ardanlabs.com/blog/2013/09/pool-go-routines-to-process-task.html

作者:William Kennedy 譯者:lebai03 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

187 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.