在 Go 語言中管理 Concurrency 的三種方式
相信大家踏入 Go 語言的世界,肯定是被強大的并發(fā)(Concurrency)所吸引,Go 語言用最簡單的關(guān)鍵字go就可以將任務(wù)丟到后臺處理,但是開發(fā)者怎么有效率的控制并發(fā),這是入門 Go 語言必學的技能,本章會介紹幾種方式來帶大家認識并發(fā),而這三種方式分別對應(yīng)到三個不同的名詞:WaitGroup,Channel,及 Context。下面用簡單的范例帶大家了解。
WaitGroup
先來了解有什么情境需要使用到 WaitGroup,假設(shè)您有兩臺機器需要同時上傳最新的代碼,兩臺機器分別上傳完成后,才能執(zhí)行最后的重啟步驟。就像是把一個工作同時拆成好幾份同時一起做,可以減少時間,但是最后需要等到全部做完,才能執(zhí)行下一步,這時候就需要用到 WaitGroup 才能做到。
package?main
import?(
????"fmt"
????"sync"
)
func?main()?{
????var?wg?sync.WaitGroup
????i?:=?0
????wg.Add(3)?//task?count?wait?to?do
????go?func()?{
????????defer?wg.Done()?//?finish?task1
????????fmt.Println("goroutine?1?done")
????????i++
????}()
????go?func()?{
????????defer?wg.Done()?//?finish?task2
????????fmt.Println("goroutine?2?done")
????????i++
????}()
????go?func()?{
????????defer?wg.Done()?//?finish?task3
????????fmt.Println("goroutine?3?done")
????????i++
????}()
????wg.Wait()?//?wait?for?tasks?to?be?done
????fmt.Println("all?goroutine?done")
????fmt.Println(i)
}
Channel
另外一種實際的案例就是,我們需要主動通知一個 Goroutine 進行停止的動作。換句話說,當 App 啟動時,會在后臺跑一些監(jiān)控程序,而當整個 App 需要停止前,需要發(fā)個 Notification 給后臺的監(jiān)控程序,將其先停止,這時候就需要用到 Channel 來通知。看下下面這個例子:
package?main
import?(
????"fmt"
????"time"
)
func?main()?{
????exit?:=?make(chan?bool)
????go?func()?{
????????for?{
????????????select?{
????????????case?<-exit:
????????????????fmt.Println("Exit")
????????????????return
????????????case?<-time.After(2?*?time.Second):
????????????????fmt.Println("Monitoring")
????????????}
????????}
????}()
????time.Sleep(5?*?time.Second)
????fmt.Println("Notify?Exit")
????exit?<-?true?//keep?main?goroutine?alive
????time.Sleep(5?*?time.Second)
}
上面的例子可以發(fā)現(xiàn),用了一個 Gogourtine 和 Channel 來控制。可以想像當后臺有無數(shù)個 Goroutine 的時候,我們就需要用多個 Channel 才能進行控制,也許 Goroutine 內(nèi)又會產(chǎn)生 Goroutine,開發(fā)者這時候就會發(fā)現(xiàn)已經(jīng)無法單純使用 Channel 來控制多個 Goroutine 了。這時候解決方式會是傳遞 Context。
Context
大家可以想像,今天有一個后臺任務(wù) A,A 任務(wù)又產(chǎn)生了 B 任務(wù),B 任務(wù)又產(chǎn)生了 C 任務(wù),也就是可以按照此模式一直產(chǎn)生下去,假設(shè)中途我們需要停止 A 任務(wù),而 A 又必須告訴 B 及 C 要一起停止,這時候通過 context 方式是最快的了。
package?main
import?(
????"context"
????"fmt"
????"time"
)
func?foo(ctx?context.Context,?name?string)?{
????go?bar(ctx,?name)?//?A?calls?B
????for?{
????????select?{
????????case?<-ctx.Done():
????????????fmt.Println(name,?"A?Exit")
????????????return
????????case?<-time.After(1?*?time.Second):
????????????fmt.Println(name,?"A?do?something")
????????}
????}
}
func?bar(ctx?context.Context,?name?string)?{
????for?{
????????select?{
????????case?<-ctx.Done():
????????????fmt.Println(name,?"B?Exit")
????????????return
????????case?<-time.After(2?*?time.Second):
????????????fmt.Println(name,?"B?do?something")
????????}
????}
}
func?main()?{
????ctx,?cancel?:=?context.WithCancel(context.Background())
????go?foo(ctx,?"FooBar")
????fmt.Println("client?release?connection,?need?to?notify?A,?B?exit")
????time.Sleep(5?*?time.Second)
????cancel()?//mock?client?exit,?and?pass?the?signal,?ctx.Done()?gets?the?signal??time.Sleep(3?*?time.Second)
????time.Sleep(3?*?time.Second)
}
大家可以把 context 想成是一個 controller,可以隨時控制不確定個數(shù)的 Goroutine,由上往下,只要宣告context.WithCancel后,再任意時間點都可以通過cancel()來停止整個后臺服務(wù)。實際案例會用在當 App 需要重新啟動時,要先通知全部 goroutine 停止,正常停止后,才會重新啟動 App。
總結(jié)
根據(jù)不同的情境跟狀況來選擇不同的方式,做一個總結(jié):
WaitGroup:需要將單一個工作分解成多個子任務(wù),等到全部完成后,才能進行下一步,這時候用 WaitGroup 最適合了 Channel + Select:Channel 只能用在比較單純的 Goroutine 情況下,如果要管理多個 Goroutine,建議還是 走 context 會比較適合 Context:如果您想一次控制全部的 Goroutine,相信用 context 會是最適合不過的,當然 context 不只有這特性,詳細可以參考『用 10 分鐘了解 Go 語言 context package 使用場景及介紹』
作者:AppleBOY
原文鏈接:https://blog.wu-boy.com/2020/08/three-ways-to-manage-concurrency-in-go/
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
