Golang 同步等待組(WaitGroup)

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同步等待組以及如何使用它實現高性能應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章