動態併發控制:sync-WaitGroup 的靈活運用

概述

在併發編程中,控制主程序等待所有 Goroutine 完成任務是一項關鍵任務。Go 語言提供了 sync.WaitGroup 來解決這一問題。

本文將講解 sync.WaitGroup 的使用方法、原理以及在實際項目中的應用場景,用清晰的代碼示例和詳細的註釋,助力讀者掌握併發編程中等待組的使用技巧。

1. 基本使用

1.1 初始化和添加計數

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var wg sync.WaitGroup
  for i := 1; i <= 3; i++ {
    wg.Add(1)
    go worker(i, &wg)
  }
  wg.Wait()
  fmt.Println("All workers have completed.")
}
func worker(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Worker %d started\n", id)
  time.Sleep(2 * time.Second)
  fmt.Printf("Worker %d completed\n", id)
}

在上面示例中,用一個 sync.WaitGroup 實例 wg,然後使用 wg.Add(1) 來增加計數,表示有一個 Goroutine 需要等待。

在每個 Goroutine 的結束處,使用 defer wg.Done() 來減少計數,表示一個 Goroutine 已完成。

最後,用 wg.Wait() 來等待所有 Goroutine 完成。

1.2 處理錯誤

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var wg sync.WaitGroup
  for i := 1; i <= 3; i++ {
    wg.Add(1)
    go workerWithError(i, &wg)
  }
  wg.Wait()
  fmt.Println("All workers have completed.")
}
func workerWithError(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Worker %d started\n", id)
  time.Sleep(2 * time.Second)
  // 模擬錯誤發生
  if id == 2 {
    fmt.Printf("Worker %d encountered an error\n", id)
    return
  }
  fmt.Printf("Worker %d completed\n", id)
}

有時候,需要在 Goroutine 中處理錯誤。在這個示例中,當 id 爲 2 時,模擬了一個錯誤的情況。

通過在錯誤發生時提前返回,可以確保計數正確減少,避免等待組出現死鎖。

2. 多級等待組

2.1 嵌套使用

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var outerWG sync.WaitGroup
  var innerWG sync.WaitGroup
  for i := 1; i <= 2; i++ {
    outerWG.Add(1)
    go outerWorker(i, &outerWG, &innerWG)
  }
  outerWG.Wait()
  fmt.Println("All outer workers have completed.")
}
func outerWorker(id int, outerWG, innerWG *sync.WaitGroup) {
  defer outerWG.Done()
  fmt.Printf("Outer Worker %d started\n", id)
  for j := 1; j <= 3; j++ {
    innerWG.Add(1)
    go innerWorker(id, j, innerWG)
  }
  innerWG.Wait()
  fmt.Printf("Outer Worker %d completed\n", id)
}
func innerWorker(outerID, innerID int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Inner Worker %d of Outer Worker %d started\n", innerID, outerID)
  time.Sleep(2 * time.Second)
  fmt.Printf("Inner Worker %d of Outer Worker %d completed\n", innerID, outerID)
}

在示例中,使用了嵌套的 sync.WaitGroup。

外部的等待組 outerWG 等待所有外部 Goroutine 完成,而每個外部 Goroutine 內部的 innerWG 則等待其內部的所有 Goroutine 完成。

2.2 動態添加等待組

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var dynamicWG sync.WaitGroup
  for i := 1; i <= 3; i++ {
    dynamicWG.Add(1)
    go dynamicWorker(i, &dynamicWG)
  }
  // 模擬動態添加更多任務
  time.Sleep(1 * time.Second)
  for i := 4; i <= 6; i++ {
    dynamicWG.Add(1)
    go dynamicWorker(i, &dynamicWG)
  }
  dynamicWG.Wait()
  fmt.Println("All dynamic workers have completed.")
}
func dynamicWorker(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Dynamic Worker %d started\n", id)
  time.Sleep(2 * time.Second)
  fmt.Printf("Dynamic Worker %d completed\n", id)
}

在上述示例中,創建了一個等待組 dynamicWG,然後在運行時動態添加了更多的任務。

用這種方式,可以動態地管理需要等待的 Goroutine 數量。

3. 超時處理

3.1 帶超時的等待

package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var timeoutWG sync.WaitGroup
  for i := 1; i <= 3; i++ {
    timeoutWG.Add(1)
    go timeoutWorker(i, &timeoutWG)
  }
  // 等待最多5秒,超時則不再等待
  timeout := time.After(5 * time.Second)
  done := make(chan struct{})
  go func() {
    timeoutWG.Wait()
    close(done)
  }()
  select {
  case <-done:
    fmt.Println("All timeout workers have completed.")
  case <-timeout:
    fmt.Println("Timeout reached. Not all workers have completed.")
  }
}
func timeoutWorker(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Timeout Worker %d started\n", id)
  time.Sleep(time.Duration(id) * time.Second)
  fmt.Printf("Timeout Worker %d completed\n", id)
}

在上面示例中,用 time.After 創建了一個 5 秒的超時通道。

在另一個 Goroutine 中監聽等待組的完成情況,可以在超時或任務完成時得知等待的最終結果。

3.2 處理超時錯誤

package main
import (
  "errors"
  "fmt"
  "sync"
  "time"
)
func main() {
  var timeoutWG sync.WaitGroup
  for i := 1; i <= 3; i++ {
    timeoutWG.Add(1)
    go timeoutWorkerWithError(i, &timeoutWG)
  }
  // 等待最多5秒,超時則返回錯誤
  err := waitWithTimeout(&timeoutWG, 5*time.Second)
  if err != nil {
    fmt.Printf("Timeout reached. Not all workers have completed. Error: %v\n", err)
  } else {
    fmt.Println("All timeout workers have completed.")
  }
}
func timeoutWorkerWithError(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Timeout Worker %d started\n", id)
  time.Sleep(time.Duration(id) * time.Second)
  // 模擬錯誤發生
  if id == 2 {
    fmt.Printf("Timeout Worker %d encountered an error\n", id)
    return
  }
  fmt.Printf("Timeout Worker %d completed\n", id)
}
func waitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) error {
  done := make(chan struct{})
  go func() {
    defer close(done)
    wg.Wait()
  }()
  select {
  case <-done:
    return nil
  case <-time.After(timeout):
    return errors.New("timeout reached")
  }
}

有時候,希望在程序超時的時候返回一個錯誤。

在這個示例中,用封裝等待組的超時檢查,可以在主程序中獲得一個清晰的錯誤提示。

總結

通過討論 sync.WaitGroup 的基本用法、避免常見錯誤以及實際應用,深入瞭解了這個強大的同步工具。

在 Go 語言併發編程中,合理使用 sync.WaitGroup 能夠優雅地處理併發等待,確保主程序在所有任務完成後再繼續執行。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/c9xf1ILV4nf17qIJGsKE1Q