相信大家踏入 Go 語言的世界,肯定是被強大的並發(Concurrency)所吸引,Go 語言用最簡單的關鍵字go就可以將任務丟到後臺處理,但是開發者怎麼有效率的控制並發,這是入門 Go 語言必學的技能,本章會介紹幾種方式來帶大家認識並發,而這三種方式分別對應到三個不同的名詞:WaitGroup,Channel,及 Context。下面用簡單的範例帶大家了解。
先來了解有什麼情境需要使用到 WaitGroup,假設您有兩臺機器需要同時上傳最新的代碼,兩臺機器分別上傳完成後,才能執行最後的重啟步驟。就像是把一個工作同時拆成好幾份同時一起做,可以減少時間,但是最後需要等到全部做完,才能執行下一步,這時候就需要用到 WaitGroup 才能做到。
package mainimport ( &34; &34;)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(&34;) i++ }() go func() { defer wg.Done() // finish task2 fmt.Println(&34;) i++ }() go func() { defer wg.Done() // finish task3 fmt.Println(&34;) i++ }() wg.Wait() // wait for tasks to be done fmt.Println(&34;) fmt.Println(i)}
另外一種實際的案例就是,我們需要主動通知一個 Goroutine 進行停止的動作。換句話說,當 App 啟動時,會在後臺跑一些監控程序,而當整個 App 需要停止前,需要發個 Notification 給後臺的監控程序,將其先停止,這時候就需要用到 Channel 來通知。看下下面這個例子:
package mainimport ( &34; &34;)func main() { exit := make(chan bool) go func() { for { select { case <-exit: fmt.Println(&34;) return case <-time.After(2 * time.Second): fmt.Println(&34;) } } }() time.Sleep(5 * time.Second) fmt.Println(&34;) exit <- true //keep main goroutine alive time.Sleep(5 * time.Second)}
上面的例子可以發現,用了一個 Gogourtine 和 Channel 來控制。可以想像當後臺有無數個 Goroutine 的時候,我們就需要用多個 Channel 才能進行控制,也許 Goroutine 內又會產生 Goroutine,開發者這時候就會發現已經無法單純使用 Channel 來控制多個 Goroutine 了。這時候解決方式會是傳遞 Context。
大家可以想像,今天有一個後臺任務 A,A 任務又產生了 B 任務,B 任務又產生了 C 任務,也就是可以按照此模式一直產生下去,假設中途我們需要停止 A 任務,而 A 又必須告訴 B 及 C 要一起停止,這時候通過 context 方式是最快的了。
package mainimport ( &34; &34; &34;)func foo(ctx context.Context, name string) { go bar(ctx, name) // A calls B for { select { case <-ctx.Done(): fmt.Println(name, &34;) return case <-time.After(1 * time.Second): fmt.Println(name, &34;) } }}func bar(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name, &34;) return case <-time.After(2 * time.Second): fmt.Println(name, &34;) } }}func main() { ctx, cancel := context.WithCancel(context.Background()) go foo(ctx, &34;) fmt.Println(&34;) 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)}
package mainimport ( &34; &34; &34;)func foo(ctx context.Context, name string) { go bar(ctx, name) // A calls B for { select { case <-ctx.Done(): fmt.Println(name, &34;) return case <-time.After(1 * time.Second): fmt.Println(name, &34;) } }}func bar(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name, &34;) return case <-time.After(2 * time.Second): fmt.Println(name, &34;) } }}func main() { ctx, cancel := context.WithCancel(context.Background()) go foo(ctx, &34;) fmt.Println(&34;) 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,可以隨時控制不確定個數的 Goroutine,由上往下,只要宣告context.WithCancel後,再任意時間點都可以通過cancel()來停止整個後臺服務。實際案例會用在當 App 需要重新啟動時,要先通知全部 goroutine 停止,正常停止後,才會重新啟動 App。
根據不同的情境跟狀況來選擇不同的方式,做一個總結:
作者:AppleBOY
原文連結:https://blog.wu-boy.com/2020/08/three-ways-to-manage-concurrency-in-go/