首页 > 后端开发 > Golang > 正文

Golang使用WaitGroup等待多任务完成实践

P粉602998670
发布: 2025-09-13 08:40:02
原创
238人浏览过
答案:sync.WaitGroup用于等待一组Goroutine完成任务,通过Add()增加计数、Done()减少计数、Wait()阻塞直至计数归零,解决主Goroutine过早退出和任务同步问题,常与channel和Mutex配合使用,需注意Add/Done调用时机、避免闭包陷阱并结合defer使用。

golang使用waitgroup等待多任务完成实践

在Go语言中,

sync.WaitGroup
登录后复制
是一个非常实用的并发原语,它主要用于等待一组 Goroutine 完成它们的任务。简单来说,它就像一个计数器,你可以增加它,表示有新的任务加入;你可以减少它,表示有任务完成了;然后你可以在主 Goroutine 中“等待”这个计数器归零,确保所有任务都已妥善处理完毕。它提供了一种简洁而强大的机制,来协调主 Goroutine 和其衍生的子 Goroutine 之间的生命周期同步。

解决方案

sync.WaitGroup
登录后复制
的核心机制围绕着三个方法:
Add()
登录后复制
Done()
登录后复制
Wait()
登录后复制

  1. Add(delta int)
    登录后复制
    : 用于增加
    WaitGroup
    登录后复制
    的内部计数器。通常在启动新的 Goroutine 之前调用,告知
    WaitGroup
    登录后复制
    有多少个任务需要等待。如果你知道需要等待的 Goroutine 数量,可以直接
    wg.Add(N)
    登录后复制
    ;如果是在循环中启动 Goroutine,则可以在每次循环迭代时
    wg.Add(1)
    登录后复制
  2. Done()
    登录后复制
    : 用于减少
    WaitGroup
    登录后复制
    的内部计数器。每个 Goroutine 完成其任务后,都应该调用
    wg.Done()
    登录后复制
    来通知
    WaitGroup
    登录后复制
    它已经完成。通常,为了确保即使 Goroutine 发生 panic 也能正确计数,我们会使用
    defer wg.Done()
    登录后复制
  3. Wait()
    登录后复制
    : 阻塞调用它的 Goroutine(通常是主 Goroutine),直到
    WaitGroup
    登录后复制
    的内部计数器归零。这意味着所有通过
    Add()
    登录后复制
    注册的任务都已通过
    Done()
    登录后复制
    完成。

下面是一个基础的实践示例:

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 确保在函数退出时通知 WaitGroup
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Duration(id) * time.Second) // 模拟工作
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 3

    fmt.Println("Main: Starting workers...")
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1) // 每启动一个 worker,计数器加 1
        go worker(i, &wg)
    }

    fmt.Println("Main: Waiting for workers to complete...")
    wg.Wait() // 阻塞主 Goroutine,直到所有 worker 都完成
    fmt.Println("Main: All workers completed. Exiting.")
}
登录后复制

运行上述代码,你会看到主 Goroutine 会等待所有

worker
登录后复制
Goroutine 完成各自的模拟工作后才打印出“All workers completed. Exiting.”,这正是
WaitGroup
登录后复制
的作用。

立即学习go语言免费学习笔记(深入)”;

为什么我们需要WaitGroup,它解决了哪些并发问题?

在我看来,Go 并发编程最让人头疼的不是如何启动多个 Goroutine,而是如何知道它们何时结束,以及如何优雅地协调这些任务。设想一下,你启动了十几个 Goroutine 去处理数据、发送请求,而你的主程序却一头雾水,不知道这些“小弟”们干得怎么样了,甚至可能在它们完成之前就直接退出了。这不就乱套了吗?

WaitGroup
登录后复制
正是来解决这类“协调与等待”问题的。它提供了一个简单的计数器机制,完美地充当了 Goroutine 之间的“同步屏障”。具体来说,它解决了以下几个核心并发问题:

  • 主 Goroutine 过早退出: 这是最常见的问题。如果没有
    WaitGroup
    登录后复制
    ,主 Goroutine 可能会在它启动的子 Goroutine 还没来得及执行甚至完成之前就退出,导致子 Goroutine 的工作被中断,或者根本没有机会开始。
    WaitGroup
    登录后复制
    通过
    Wait()
    登录后复制
    方法,强制主 Goroutine 等待所有子 Goroutine 完成。
  • 任务集合的完成状态同步: 当你需要确保一个批次的所有并发任务都已完成,才能进行下一步操作时,
    WaitGroup
    登录后复制
    是理想的选择。例如,你可能需要等待所有文件下载完毕才能进行合并,或者所有数据库查询完成后才能汇总结果。
  • 简化并发流程管理: 相较于手动使用
    channel
    登录后复制
    来发送完成信号,
    WaitGroup
    登录后复制
    在这种“等待一组任务完成”的场景下,提供了更简洁、更直观的 API。你不需要关心
    channel
    登录后复制
    的缓冲大小,也不需要循环接收完成信号。

它本质上是一个“计数器”,

Add()
登录后复制
增加计数,
Done()
登录后复制
减少计数,
Wait()
登录后复制
则等待计数归零。这种简单而强大的模型,让并发任务的协调变得清晰可控。

WaitGroup与Channel、Mutex等其他并发原语有何不同?

Go 的并发工具箱里宝贝不少,

WaitGroup
登录后复制
只是其中之一。但它和
channel
登录后复制
Mutex
登录后复制
这些明星选手,职责和用法上可是大相径庭的。理解它们的区别,能帮助我们更好地选择合适的工具来解决特定的并发问题。

  • sync.WaitGroup
    登录后复制
    :侧重于“等待完成”的同步
    WaitGroup
    登录后复制
    的核心功能是同步一组 Goroutine 的完成。它不负责数据传输,也不负责保护共享资源。它就像一个项目经理,只关心所有任务是否都按时完成了,而不关心任务具体是怎么完成的,或者任务之间有没有传递数据。它的主要职责就是让一个 Goroutine (通常是主 Goroutine) 阻塞,直到所有它负责启动的子 Goroutine 都发出了完成信号。

  • chan
    登录后复制
    (Channel):侧重于“通信和协调”
    channel
    登录后复制
    是 Go 语言中最核心的并发原语,它不仅仅用于同步,更重要的是用于 Goroutine 之间的安全通信。通过
    channel
    登录后复制
    ,一个 Goroutine 可以向另一个 Goroutine 发送数据,或者接收数据。这种通信本身就带有同步的性质(发送和接收都会阻塞),但它的主要目的是数据交换。你当然可以用
    channel
    登录后复制
    来实现类似
    WaitGroup
    登录后复制
    的功能(比如每个 Goroutine 完成后向
    channel
    登录后复制
    发送一个信号,主 Goroutine 接收 N 个信号),但那样会更复杂,且不是
    channel
    登录后复制
    的最佳应用场景。

  • sync.Mutex
    登录后复制
    (互斥锁):侧重于“保护共享资源”
    Mutex
    登录后复制
    的作用是确保在任何给定时刻,只有一个 Goroutine 可以访问特定的共享资源(如变量、映射、结构体字段等)。它解决了数据竞争(data race)问题。当多个 Goroutine 尝试同时修改同一块内存时,如果没有
    Mutex
    登录后复制
    保护,结果将是不可预测的。
    Mutex
    登录后复制
    通过
    Lock()
    登录后复制
    Unlock()
    登录后复制
    方法,强制对共享资源的串行访问。
    WaitGroup
    登录后复制
    根本不涉及共享资源的保护,它只关心任务的完成状态。

总结来说:

  • WaitGroup
    登录后复制
    :用于等待一组 Goroutine 完成。
  • channel
    登录后复制
    :用于 Goroutine 之间安全地通信和协调。
  • Mutex
    登录后复制
    :用于保护共享资源,防止数据竞争。

它们是互补而非互斥的。在复杂的并发场景中,我们经常会看到它们协同工作。比如,你可能会用

WaitGroup
登录后复制
等待所有工作 Goroutine 完成,同时用
channel
登录后复制
来收集这些 Goroutine 处理后的结果,再用
Mutex
登录后复制
来保护一个共享的计数器或映射,以确保结果的正确性。

ViiTor实时翻译
ViiTor实时翻译

AI实时多语言翻译专家!强大的语音识别、AR翻译功能。

ViiTor实时翻译 116
查看详情 ViiTor实时翻译

在实际项目中,使用WaitGroup有哪些常见的陷阱或最佳实践?

实践出真知,但实践中也容易踩坑。

WaitGroup
登录后复制
虽然简单,用不好也能让人头疼。我在实际项目中,也遇到过一些让人抓狂的
WaitGroup
登录后复制
相关问题。

常见的陷阱:

  1. Add()
    登录后复制
    调用时机不当

    • 问题:如果在启动 Goroutine 之后才调用
      wg.Add(1)
      登录后复制
      ,或者在
      wg.Wait()
      登录后复制
      之后又调用
      wg.Add(1)
      登录后复制
      ,可能会导致两种情况:
      • 死锁:如果
        wg.Wait()
        登录后复制
        已经在等待,而此时
        Add(1)
        登录后复制
        使得计数器再次大于零,
        Wait()
        登录后复制
        将永远不会返回。
      • 竞态条件:如果
        Add(1)
        登录后复制
        发生在某个 Goroutine 已经
        Done()
        登录后复制
        之后,
        WaitGroup
        登录后复制
        的计数可能无法正确反映实际的 Goroutine 数量。
    • 示例(错误):
      // wg.Add(1) 在 go func() 之后,可能导致问题
      // for i := 0; i < 5; i++ {
      //     go func() {
      //         defer wg.Done()
      //         fmt.Println("Worker done")
      //     }()
      //     wg.Add(1) // 错误!
      // }
      登录后复制
  2. 忘记

    defer wg.Done()
    登录后复制

    • 问题:如果 Goroutine 在执行过程中发生 panic,或者由于某种逻辑分支没有执行到
      wg.Done()
      登录后复制
      ,那么
      WaitGroup
      登录后复制
      的计数器将永远不会归零,导致
      wg.Wait()
      登录后复制
      永远阻塞,造成死锁。
    • 示例(错误):
      // func worker(wg *sync.WaitGroup) {
      //     // 如果这里发生 panic,wg.Done() 将不会被调用
      //     // wg.Done()
      // }
      登录后复制
  3. WaitGroup
    登录后复制
    计数器操作不平衡

    • 问题:调用
      wg.Done()
      登录后复制
      的次数多于
      wg.Add()
      登录后复制
      的次数,会导致计数器变成负数,这将引发
      panic
      登录后复制
      。反之,如果
      Done()
      登录后复制
      次数少于
      Add()
      登录后复制
      次数,则会导致死锁。
  4. Goroutine 闭包陷阱

    • 问题:在循环中启动 Goroutine 时,如果 Goroutine 内部引用了循环变量,它会捕获循环变量的最终值,而不是每次迭代时的值。
    • 示例(错误):
      // for i := 0; i < 5; i++ {
      //     wg.Add(1)
      //     go func() {
      //         defer wg.Done()
      //         fmt.Printf("Worker %d\n", i) // 这里的 i 最终会是 4 或 5
      //     }()
      // }
      登录后复制

最佳实践:

  1. wg.Add()
    登录后复制
    始终在启动 Goroutine 之前调用:这是最基本也最重要的规则。确保
    WaitGroup
    登录后复制
    在开始等待之前,已经正确地注册了所有需要等待的任务。

    // 正确的做法
    for i := 0; i < numWorkers; i++ {
        wg.Add(1) // 先增加计数
        go worker(i, &wg) // 再启动 Goroutine
    }
    登录后复制

    或者,如果 Goroutine 数量是固定的,可以直接

    wg.Add(numWorkers)
    登录后复制
    一次性增加。

  2. 始终使用

    defer wg.Done()
    登录后复制
    :在 Goroutine 函数的开头立即
    defer wg.Done()
    登录后复制
    ,可以确保无论 Goroutine 正常完成还是发生 panic,计数器都会被正确递减。

    func worker(id int, wg *sync.WaitGroup) {
        defer wg.Done() // 确保在函数退出时通知 WaitGroup
        // ... 业务逻辑 ...
    }
    登录后复制
  3. 处理 Goroutine 闭包陷阱:将循环变量作为参数传递给 Goroutine 函数,或者在循环内部创建一个局部变量来捕获当前迭代的值。

    // 正确的做法:将 i 作为参数传递
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) { // id 是一个新的局部变量
            defer wg.Done()
            fmt.Printf("Worker %d\n", id)
        }(i) // 将 i 的当前值传递给 Goroutine
    }
    
    // 或者在循环内部创建新变量
    for i := 0; i < 5; i++ {
        wg.Add(1)
        taskID := i // 创建一个当前 i 值的副本
        go func() {
            defer wg.Done()
            fmt.Printf("Worker %d\n", taskID)
        }()
    }
    登录后复制
  4. 错误处理和上下文(Context)结合使用

    WaitGroup
    登录后复制
    仅仅等待任务完成,它不提供错误传播或取消机制。对于需要错误处理或超时取消的场景,应该结合
    channel
    登录后复制
    来传递错误,或结合
    context.Context
    登录后复制
    来实现取消和超时。例如,Goroutine 可以通过
    channel
    登录后复制
    将错误发送回主 Goroutine,主 Goroutine 在
    wg.Wait()
    登录后复制
    之后或在另一个 Goroutine 中监听这些错误
    channel
    登录后复制

遵循这些最佳实践,可以大大减少在使用

WaitGroup
登录后复制
时遇到的问题,让你的并发代码更加健壮和可靠。

以上就是Golang使用WaitGroup等待多任务完成实践的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门推荐
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号