waitgroup的使用

在 Go 语言中,sync.WaitGroup 是用于等待一组 Goroutine 完成执行的同步工具,常用于主 Goroutine 协调多个子 Goroutine 的生命周期。以下是其核心用法及注意事项:


1. 基本用法

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 确保任务完成时调用 Done

    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // 模拟耗时任务
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // 每启动一个 Goroutine,计数器 +1
        go worker(i, &wg) // 传递 WaitGroup 的指针
    }

    wg.Wait() // 阻塞,直到所有 Goroutine 完成
    fmt.Println("All workers completed")
}

输出

Worker 3 starting
Worker 1 starting
Worker 2 starting
Worker 1 done
Worker 2 done
Worker 3 done
All workers completed

2. 核心方法

方法 作用
wg.Add(n int) 增加等待的 Goroutine 数量(通常在启动 Goroutine 前调用)
wg.Done() 标记一个 Goroutine 完成(等价于 wg.Add(-1),建议用 defer 调用)
wg.Wait() 阻塞主 Goroutine,直到所有子 Goroutine 完成(计数器归零)

3. 注意事项

(1) 正确传递指针

  • 错误示例(值传递导致死锁):

    go worker(i, wg) // 值传递,Done() 不影响原 WaitGroup
    
  • 正确方式

    go worker(i, &wg) // 传递指针
    

(2) 确保 Add 和 Done 匹配

  • 错误示例(Add 次数不足):

    wg.Add(1)
    for i := 0; i < 3; i++ {
        go func() { wg.Done() }() // 触发 panic:计数器为负
    }
    
  • 正确方式

    wg.Add(3) // 明确指定需要等待 3 次 Done
    for i := 0; i < 3; i++ {
        go func() { wg.Done() }()
    }
    

(3) 使用 defer 调用 Done

避免因 Goroutine 提前退出或 panic 导致 Done() 未执行:

func worker(wg *sync.WaitGroup) {
    defer wg.Done() // 确保 Done 一定执行
    // ...(可能 panic 的代码)
}

4. 常见场景

(1) 批量任务等待

var wg sync.WaitGroup
urls := []string{"https://example.com", "https://google.com"}

for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        resp, _ := http.Get(u)
        defer resp.Body.Close()
    }(url)
}

wg.Wait()
fmt.Println("All HTTP requests completed")

(2) 结合错误处理

使用通道收集子 Goroutine 的错误:

package main

import (
	"fmt"
	"sync"
)

func main() {

	var wg sync.WaitGroup
	errCh := make(chan error, 3)

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			if err := doTask(id); err != nil {
				errCh <- err
			}
		}(i)
	}

	go func() {
		wg.Wait()
		close(errCh) // 关闭通道,避免主 Goroutine 死锁
	}()

	for err := range errCh {
		fmt.Println("Error:", err) // Error: 0 is not equal to 2  //	Error: 2 is not equal to 2
	}
}

func doTask(i int) error {

	if i != 1 {
		return fmt.Errorf("%d is not equal to 2", i)
	}
	return nil

}

5. 死锁陷阱

未调用 Wait

wg.Add(1)
go func() { wg.Done() }()
// 缺少 wg.Wait(),主 Goroutine 直接退出

Done 调用次数超过 Add

wg.Add(1)
go func() {
    wg.Done()
    wg.Done() // panic: negative WaitGroup counter
}()

6. errgroup

鉴于使用 sync.WaitGroup 容易出错,Go 官方包 errgroup 对 sync.WaitGroup 做了进一步封装,不再需要 Add 和 Done,用起来更加方便。详见 golang.org/x/sync/errgroup

// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero Group is valid and does not cancel on error.
type Group struct {
	cancel func()

	wg sync.WaitGroup

	errOnce sync.Once
	err     error
}

使用示例:

package main

import (
	"fmt"

	"golang.org/x/sync/errgroup"
)

func foo1() error {
	fmt.Println("exit foo1")
	return nil
}

func foo2() error {
	fmt.Println("exit foo2")
	return nil
}

func main() {
	fmt.Println("enter main")
	var g errgroup.Group
	g.Go(foo1)
	g.Go(foo2)

	fmt.Println("g.Wait()")
	g.Wait()

	fmt.Println("exit main")
}

运行输出:

enter main
wg.Wait()
exit foo2
exit foo1
exit main

总结

  • 何时使用:需要等待多个 Goroutine 全部完成后继续执行主逻辑。
  • 最佳实践
    • 始终通过指针传递 WaitGroup
    • 使用 defer wg.Done() 确保资源释放。
    • 在启动 Goroutine 调用 Add,避免竞态条件。
  • 替代方案:简单场景可用 channelerrgroup.Group(支持错误传播)。
posted @   搁浅~浅浅浅  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示