Golang 同步等待組(WaitGroup)
如果你正在學習Go的高性能併發應用開發,那麼瞭解同步等待組至關重要。本文帶你認識同步等待組並通過示例進行說明。
1. 同步等待組(WaitGroup)
讓我們直入主題,說明是同步等待組(WaitGroup),能夠解決什麼問題。
在實際使用Go協程實現並行應用時,可能會遇到這樣場景:需要阻塞部分代碼執行,直到其他協程成功執行之後才繼續執行。
示例代碼:
package main
import "fmt"
func myFunc() {
fmt.Println("Inside my goroutine")
}
func main() {
fmt.Println("Hello World")
go myFunc()
fmt.Println("Finished Execution")
}
程序首先打印"Hello World",接着啓動協程,最後打印"Finished Execution"。
但我們執行程序結果並不是我們預期的結果,協程內的信息"Inside my goroutine"並沒有出現。這是因爲main在協程執行之前以及結束,所以協程中的邏輯並未執行。
如何解決————同步等待組(WaitGroups)
同步等待組(WaitGroups)就是要解決這類問題,阻塞應用直到同步等待組中的所有協程都成功執行。
首先調用同步等待組的Add(1)方法,設置需要等待協程數量, 然後再協程內部調用Done()
方法表明協程執行結束。
注意,需要確保再執行協程之前調用
Add(1)
方法。
2. 示例
掌握了一些基本概念後,下面通過示例展示如何通過同步等待組解決上述問題:
package main
import (
"fmt"
"sync"
)
func myFunc(waitgroup *sync.WaitGroup) {
fmt.Println("Inside my goroutine")
waitgroup.Done()
}
func main() {
fmt.Println("Hello World")
var waitgroup sync.WaitGroup
waitgroup.Add(1)
go myFunc(&waitgroup)
waitgroup.Wait()
fmt.Println("Finished Execution")
}
我們看到首先實例化sync.WaitGroup
,然後再執行協程之前調用Add(1)
方法。修改原來函數增加*sync.WaitGroup
參數,並在函數內部成功完成任務後調用一次Done
方法。最後調用waitgroup.Wait()
方法阻塞main函數執行,直到同步等待組中的協程成功執行完成。
下面再次運行程序輸出結果如下:
Hello World
Inside my goroutine
Finished Execution
- 匿名函數
我們也可以使用匿名函數實現相同功能。對於協程內部業務不復雜,匿名函數會讓程序更簡潔:
package main
import (
"fmt"
"sync"
)
func main() {
fmt.Println("Hello World")
var waitgroup sync.WaitGroup
waitgroup.Add(1)
go func() {
fmt.Println("Inside my goroutine")
waitgroup.Done()
}()
waitgroup.Wait()
fmt.Println("Finished Execution")
}
輸出結果一樣。對於稍微複雜的邏輯,可能需要給匿名函數傳入參數,例如需要傳入url參數:
go func(url string) {
fmt.Println(url)
}(url)
只是寫法有點差異,其他都差不多。
3. 實戰應用
在示例生產應用程序中,任務是創建一個API,該API與大量其他API交互,並將結果聚合到一個響應中。每個API調用大約花費2-3秒時間來返回響應,由於需要調用的API數量太多,不可能同步地進行此操作。
爲了實現該功能,需要使用協程異步執行這些請求。
package main
import (
"fmt"
"log"
"net/http"
)
var urls = []string{
"https://baidu.com",
"https://csdn.net",
"https://qq.com",
}
func fetch(url string) {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Status)
}
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Println("HomePage Endpoint Hit")
for _, url := range urls {
go fetch(url)
}
fmt.Println("Returning Response")
fmt.Fprintf(w, "All Responses Received")
}
func handleRequests() {
http.HandleFunc("/", homePage)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func main() {
handleRequests()
}
然而當我開始使用這種策略時,我注意到我對任何API調用都在協程有機會完成填充結果之前返回。這時需要使用同步等待組重新實現該功能,通過使用WaitGroup,我可以有效地修復這種異常行爲,並且只在所有goroutines完成後返回結果。
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var urls = []string{
"https://baidu.com",
"https://csdn.net",
"https://qq.com",
}
func fetch(url string, wg *sync.WaitGroup) (string, error) {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
return "", err
}
wg.Done()
fmt.Println(resp.Status)
return resp.Status, nil
}
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Println("HomePage Endpoint Hit")
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go fetch(url, &wg)
}
wg.Wait()
fmt.Println("Returning Response")
fmt.Fprintf(w, "Responses")
}
func handleRequests() {
http.HandleFunc("/", homePage)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func main() {
handleRequests()
}
現在我們增加同步等待鎖,它將對所有URL執行HTTP GET請求,一旦執行完畢返回給調用客戶端。輸出結果爲:
HomePage Endpoint Hit
200 OK
200 OK
200 OK
Returning Response
處理這類問題的方法不止一種,通過使用Golang的通道也可以實現類似功能。
4. 總結
本文學習了什麼是Golang同步等待組以及如何使用它實現高性能應用。