0

0

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

P粉602998670

P粉602998670

发布时间:2025-09-13 08:40:02

|

243人浏览过

|

来源于php中文网

原创

答案: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
来保护一个共享的计数器或映射,以确保结果的正确性。

SUN2008 企业网站管理系统2.0 beta
SUN2008 企业网站管理系统2.0 beta

1、数据调用该功能使界面与程序分离实施变得更加容易,美工无需任何编程基础即可完成数据调用操作。2、交互设计该功能可以方便的为栏目提供个性化性息功能及交互功能,为产品栏目添加产品颜色尺寸等属性或简单的留言和订单功能无需另外开发模块。3、静态生成触发式静态生成。4、友好URL设置网页路径变得更加友好5、多语言设计1)UTF8国际编码; 2)理论上可以承担一个任意多语言的网站版本。6、缓存机制减轻服务器

下载

在实际项目中,使用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如何定义变量
golang如何定义变量

golang定义变量的方法:1、声明变量并赋予初始值“var age int =值”;2、声明变量但不赋初始值“var age int”;3、使用短变量声明“age :=值”等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

178

2024.02.23

golang有哪些数据转换方法
golang有哪些数据转换方法

golang数据转换方法:1、类型转换操作符;2、类型断言;3、字符串和数字之间的转换;4、JSON序列化和反序列化;5、使用标准库进行数据转换;6、使用第三方库进行数据转换;7、自定义数据转换函数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

226

2024.02.23

golang常用库有哪些
golang常用库有哪些

golang常用库有:1、标准库;2、字符串处理库;3、网络库;4、加密库;5、压缩库;6、xml和json解析库;7、日期和时间库;8、数据库操作库;9、文件操作库;10、图像处理库。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

337

2024.02.23

golang和python的区别是什么
golang和python的区别是什么

golang和python的区别是:1、golang是一种编译型语言,而python是一种解释型语言;2、golang天生支持并发编程,而python对并发与并行的支持相对较弱等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

208

2024.03.05

golang是免费的吗
golang是免费的吗

golang是免费的。golang是google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的开源编程语言,采用bsd开源协议。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

388

2024.05.21

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

195

2025.06.09

golang相关判断方法
golang相关判断方法

本专题整合了golang相关判断方法,想了解更详细的相关内容,请阅读下面的文章。

190

2025.06.10

golang数组使用方法
golang数组使用方法

本专题整合了golang数组用法,想了解更多的相关内容,请阅读专题下面的文章。

192

2025.06.17

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
golang socket 编程
golang socket 编程

共2课时 | 0.1万人学习

nginx浅谈
nginx浅谈

共15课时 | 0.8万人学习

golang和swoole核心底层分析
golang和swoole核心底层分析

共3课时 | 0.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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