Golang中sync.WaitGroup 等待組的實(shí)現(xiàn)
一、WaitGroup 解決什么問(wèn)題
當(dāng)我們啟動(dòng)了一組 goroutine 去干活,主 goroutine 需要在所有子 goroutine 完成之后再繼續(xù)執(zhí)行。比如:
- 并發(fā)抓取 10 個(gè)網(wǎng)頁(yè),全部抓完再匯總結(jié)果
- 批量處理文件,所有 worker 跑完再寫入?yún)R總報(bào)告
- 啟動(dòng)多個(gè)微服務(wù)健康檢查,全通過(guò)才算就緒
WaitGroup 就是為這類"等待一組并發(fā)任務(wù)完成"的訴求設(shè)計(jì)的。
二、三個(gè)核心方法
WaitGroup 的 API 極為簡(jiǎn)潔——只有 Add、Done、Wait 三個(gè)方法:
var wg sync.WaitGroup
// Add(n): 計(jì)數(shù)器加 n,表示"我又派了 n 個(gè)任務(wù)"
wg.Add(3)
for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done() // 等價(jià)于 Add(-1),表示"我干完了"
// ... 干活 ...
}(i)
}
wg.Wait() // 阻塞直到計(jì)數(shù)器歸零
Done 內(nèi)部就是 wg.Add(-1),二者語(yǔ)義完全等價(jià)。
三、設(shè)計(jì)哲學(xué):誰(shuí)派生,誰(shuí) Add
這是最容易踩坑的地方。Add 必須在啟動(dòng) goroutine 之前調(diào)用,否則可能 Wait 先執(zhí)行,直接放行:
// ? 錯(cuò)誤:wg.Add 在 go 里面
for i := 0; i < 5; i++ {
go func() {
wg.Add(1) // 嚴(yán)重錯(cuò)誤!Wait 可能在這之前就返回了
defer wg.Done()
doWork()
}()
}
wg.Wait()
正確的做法是 Add 和 go 語(yǔ)句緊挨在一起:
// ? 正確
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
四、WaitGroup 是值類型,必須傳指針
WaitGroup 結(jié)構(gòu)體內(nèi)部維護(hù)了一個(gè)計(jì)數(shù)器,不能用做值拷貝的參數(shù)傳遞——副本中的加減不會(huì)影響原值。
// ? 值傳遞:計(jì)數(shù)器的修改對(duì)原始 wg 不可見(jiàn)
func run(wg sync.WaitGroup) {
defer wg.Done()
}
// ? 指針傳遞
func run(wg *sync.WaitGroup) {
defer wg.Done()
}
Go 1.20+ 編譯器會(huì)給出 copylock 警告,但不如一開(kāi)始就用對(duì)。
五、WaitGroup 不能重用"飛行中"的實(shí)例
WaitGroup 的內(nèi)部狀態(tài)有三個(gè)階段:0 → Add → Wait。Wait 返回后,WaitGroup 自動(dòng)回到 0 狀態(tài)可以重用。但如果在 Wait 還沒(méi)返回時(shí)又調(diào)用 Add,這屬于并發(fā)訪問(wèn)內(nèi)部狀態(tài),是數(shù)據(jù)競(jìng)爭(zhēng)。
// ? 數(shù)據(jù)競(jìng)爭(zhēng)!
go func() {
wg.Add(1) // 與下面的 wg.Wait 并發(fā)
defer wg.Done()
doWork()
}()
wg.Wait() // 此時(shí)可能還沒(méi)有 Add 完成
六、練習(xí)代碼
保存為 waitgroup_demo.go:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 模擬從多個(gè)數(shù)據(jù)源獲取數(shù)據(jù)
type DataFetcher struct {
name string
latency time.Duration // 模擬耗時(shí)
}
func (f DataFetcher) Fetch() string {
time.Sleep(f.latency)
return fmt.Sprintf("[%s 數(shù)據(jù)]", f.name)
}
func main() {
rand.Seed(time.Now().UnixNano())
// 三個(gè)數(shù)據(jù)源,各自耗時(shí)不同
sources := []DataFetcher{
{name: "數(shù)據(jù)庫(kù)", latency: 200 * time.Millisecond},
{name: "Redis", latency: 50 * time.Millisecond},
{name: "遠(yuǎn)程API", latency: 300 * time.Millisecond},
}
// 結(jié)果收集
results := make([]string, len(sources))
var wg sync.WaitGroup
for i, src := range sources {
wg.Add(1) // 在 go 之前 Add
go func(idx int, fetcher DataFetcher) {
defer wg.Done()
results[idx] = fetcher.Fetch()
}(i, src) // 注意:把循環(huán)變量傳給閉包
}
fmt.Println("等待所有數(shù)據(jù)源返回...")
wg.Wait()
fmt.Println("全部就緒!\n")
for i, r := range results {
fmt.Printf("數(shù)據(jù)源 %d (%s): %s\n", i, sources[i].name, r)
}
// —— 進(jìn)階:帶超時(shí)的 WaitGroup ——
fmt.Println("\n——— 超時(shí)模式演示 ———")
var wg2 sync.WaitGroup
wg2.Add(1)
go func() {
defer wg2.Done()
time.Sleep(2 * time.Second) // 模擬很慢的任務(wù)
fmt.Println("慢任務(wù)完成")
}()
// 用一個(gè) channel 來(lái)通知"WaitGroup 已經(jīng)等完了"
done := make(chan struct{})
go func() {
wg2.Wait()
close(done)
}()
select {
case <-done:
fmt.Println("所有任務(wù)在超時(shí)前完成")
case <-time.After(500 * time.Millisecond):
fmt.Println("超時(shí)!不等了")
}
}
運(yùn)行結(jié)果:
等待所有數(shù)據(jù)源返回...
全部就緒!數(shù)據(jù)源 0 (數(shù)據(jù)庫(kù)): [數(shù)據(jù)庫(kù) 數(shù)據(jù)]
數(shù)據(jù)源 1 (Redis): [Redis 數(shù)據(jù)]
數(shù)據(jù)源 2 (遠(yuǎn)程API): [遠(yuǎn)程API 數(shù)據(jù)]——— 超時(shí)模式演示 ———
超時(shí)!不等了
七、WaitGroup vs Channel
| 場(chǎng)景 | WaitGroup | Channel |
|---|---|---|
| 等待所有 goroutine 結(jié)束 | ? 天然適合 | 需要逐個(gè)接收,寫起來(lái)繁瑣 |
| 需要收集返回值 | 需額外變量/鎖 | 可以直接從 channel 讀取結(jié)果 |
| 限制并發(fā)數(shù) | 不適合 | 用有緩沖 channel 做信號(hào)量 |
| 取消/超時(shí) | 需配合 select(見(jiàn)上文) | 配合 select + ctx.Done() |
八、關(guān)鍵要點(diǎn)
| 要點(diǎn) | 說(shuō)明 |
|---|---|
| Add 在前 | 必須在 go 語(yǔ)句之前調(diào)用 Add |
| defer Done | 確保 goroutine panic 也能 Done |
| 傳指針 | WaitGroup 是值類型,傳遞需取地址 |
| Wait 可重入 | 多個(gè) goroutine 可以同時(shí) Wait |
| 超時(shí)模式 | Wait 本身不超時(shí),配合 channel + select 實(shí)現(xiàn) |
| 不能復(fù)制 | 復(fù)制 WaitGroup 的值會(huì)破壞內(nèi)部狀態(tài) |
到此這篇關(guān)于Golang中sync.WaitGroup 等待組的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Golang sync.WaitGroup 等待組內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang迭代如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解
這篇文章主要為大家介紹了Golang迭代之如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Go語(yǔ)言常見(jiàn)錯(cuò)誤之將接口定義在實(shí)現(xiàn)方
在Go中,接口起到一個(gè)十分關(guān)鍵的角色,它們提供了一種方式來(lái)定義對(duì)象的行為,而不需要知道對(duì)象的具體實(shí)現(xiàn),一個(gè)常見(jiàn)的錯(cuò)誤是在實(shí)現(xiàn)方而不是使用方定義接口,本文將詳細(xì)探討為何這樣做是一個(gè)錯(cuò)誤,以及如何避免它2024-01-01
Go語(yǔ)言基礎(chǔ)if條件語(yǔ)句用法及示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)if條件語(yǔ)句的用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11
Golang使用Gin創(chuàng)建Restful API的實(shí)現(xiàn)
本文主要介紹了Golang使用Gin創(chuàng)建Restful API的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏的解決方案詳解
這篇文章主要為大家詳細(xì)介紹了當(dāng)Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏時(shí)蓋如何解決,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-07-07

