waitgroup用于等待一組goroutine完成。其核心是通過add()增加計數器,done()減少計數器(等價于add(-1)),wait()阻塞主goroutine直到計數器歸零。使用時應在啟動goroutine前調用add(),并在每個goroutine中使用defer wg.done()確保計數器正確減少。避免錯誤的方法包括:使用defer確保done()調用、通過指針傳遞waitgroup、借助工具審查代碼。與channel相比,waitgroup適用于僅需等待完成而無需數據傳遞的場景,channel則適合需要數據傳輸或復雜同步的情況。結合context可控制goroutine生命周期,如通過context.withcancel()實現優雅退出。
WaitGroup用于等待一組goroutine完成。你可以理解為一個計數器,每啟動一個goroutine,計數器加一,goroutine執行完畢,計數器減一。主goroutine調用Wait()方法阻塞,直到計數器變為零。
解決方案:
在golang中使用WaitGroup非常簡單,主要涉及三個方法:Add(), Done(), 和 Wait()。
立即學習“go語言免費學習筆記(深入)”;
- Add(delta int):增加WaitGroup的計數器。通常在啟動goroutine之前調用。delta可以是正數也可以是負數,但通常是正數,表示新增的goroutine數量。
- Done():減少WaitGroup的計數器。在goroutine執行完畢后調用。相當于Add(-1)。
- Wait():阻塞調用它的goroutine(通常是主goroutine),直到WaitGroup的計數器變為零。
下面是一個簡單的例子:
package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 確保goroutine退出時計數器減一 fmt.Printf("Worker %d startingn", id) time.Sleep(time.Second) // 模擬耗時操作 fmt.Printf("Worker %d donen", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) // 啟動一個goroutine,計數器加一 go worker(i, &wg) } wg.Wait() // 阻塞直到所有goroutine完成 fmt.Println("All workers done") }
這個例子創建了三個worker goroutine,每個goroutine休眠一秒后退出。主goroutine等待所有worker goroutine完成后才退出。
WaitGroup的零值是有效的,意味著你可以直接聲明一個sync.WaitGroup類型的變量,而不需要進行初始化。
WaitGroup的計數器不能為負數。如果計數器變為負數,會panic。因此,確保Add()的調用次數與Done()的調用次數匹配。
如何避免WaitGroup使用中的常見錯誤?
使用WaitGroup時,最常見的錯誤包括:
- 忘記Done():如果goroutine忘記調用Done(),WaitGroup將永遠不會變為零,導致Wait()永久阻塞。
- Done()調用次數過多:如果Done()的調用次數超過Add()的調用次數,會導致panic。
- Add()和goroutine啟動順序問題:如果Add()在goroutine啟動之后調用,可能導致WaitGroup計數器不準確。應該始終在啟動goroutine之前調用Add()。
- WaitGroup傳遞問題:WaitGroup應該通過指針傳遞,而不是值傳遞。值傳遞會導致每個goroutine都操作的是WaitGroup的副本,而不是同一個WaitGroup。
為了避免這些錯誤,可以考慮使用defer語句來確保Done()被調用,并且仔細檢查Add()和Done()的調用次數是否匹配。同時,使用代碼審查工具可以幫助發現潛在的錯誤。
WaitGroup和channel有什么區別?何時使用哪個?
WaitGroup和channel都是用于goroutine同步的機制,但它們的使用場景有所不同。
- WaitGroup主要用于等待一組goroutine完成。它不涉及數據的傳遞,只關注goroutine的完成狀態。
- Channel主要用于goroutine之間的數據傳遞和同步。它可以用于等待單個或多個goroutine,也可以用于在goroutine之間傳遞數據。
通常情況下,如果只需要等待一組goroutine完成,而不需要傳遞數據,那么WaitGroup是更簡單和高效的選擇。如果需要在goroutine之間傳遞數據,或者需要更復雜的同步邏輯,那么channel是更好的選擇。
例如,可以使用channel來收集多個goroutine的結果,或者使用channel來實現一個工作池。
package main import ( "fmt" "sync" ) func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) { defer wg.Done() for j := range jobs { fmt.Printf("Worker %d processing job %dn", id, j) results <- j * 2 } } func main() { numJobs := 5 jobs := make(chan int, numJobs) results := make(chan int, numJobs) var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go worker(i, jobs, results, &wg) } for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs) wg.Wait() close(results) for a := range results { fmt.Println(a) } }
這個例子使用了channel來傳遞任務和結果,同時使用WaitGroup來等待所有worker goroutine完成。
如何使用context控制goroutine的生命周期?
Context可以用于控制goroutine的生命周期,例如取消goroutine的執行。結合WaitGroup和context,可以實現更復雜的goroutine管理。
可以使用context.WithCancel()創建一個可取消的context。當調用cancel()函數時,所有監聽該context的goroutine都會收到取消信號。
package main import ( "context" "fmt" "sync" "time" ) func worker(ctx context.Context, id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d startingn", id) defer fmt.Printf("Worker %d donen", id) for { select { case <-ctx.Done(): fmt.Printf("Worker %d cancelledn", id) return default: fmt.Printf("Worker %d workingn", id) time.Sleep(time.Millisecond * 500) } } } func main() { var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) for i := 1; i <= 3; i++ { wg.Add(1) go worker(ctx, i, &wg) } time.Sleep(time.Second * 2) fmt.Println("Cancelling context") cancel() wg.Wait() fmt.Println("All workers done") }
這個例子創建了三個worker goroutine,每個goroutine會循環執行,直到收到取消信號。主goroutine在2秒后取消context,導致所有worker goroutine退出。WaitGroup用于等待所有worker goroutine退出。
使用context可以更優雅地控制goroutine的生命周期,避免資源泄漏和死鎖。