0

0

掌握 Go 语言中的 sync.WaitGroup:并发任务的同步与管理

碧海醫心

碧海醫心

发布时间:2025-11-02 11:12:44

|

290人浏览过

|

来源于php中文网

原创

掌握 Go 语言中的 sync.WaitGroup:并发任务的同步与管理

sync.waitgroup 是 go 语言中用于并发控制的重要工具,确保主 goroutine 等待所有子 goroutine 完成任务。本文深入探讨了 waitgroup 的正确使用方式,特别是 wg.add() 的放置时机,强调了其必须在 go 语句之前调用以有效避免竞态条件。我们将通过代码示例详细解析 add、done 和 wait 的协同工作机制,并解释 go 内存模型如何保证操作顺序,从而帮助开发者编写健壮的并发程序。

引言:理解 Go 并发中的 sync.WaitGroup

在 Go 语言的并发编程中,我们经常需要启动多个 goroutine 来并行执行任务。然而,主程序往往需要等待所有这些并发任务完成后才能继续执行或退出。sync.WaitGroup 就是 Go 标准库提供的一种轻量级且高效的同步原语,用于实现这种“等待所有任务完成”的机制。它允许一个 goroutine 等待一组其他 goroutine 完成它们的执行。

WaitGroup 的核心思想是维护一个内部计数器。当计数器归零时,Wait 方法就会解除阻塞。

sync.WaitGroup 的核心组件

sync.WaitGroup 主要由三个方法组成:

  1. Add(delta int): 用于增加或减少 WaitGroup 的计数器。通常,delta 是正数,表示要等待的 goroutine 数量。例如,wg.Add(1) 表示增加一个需要等待的 goroutine。
  2. Done(): 相当于 Add(-1)。当一个 goroutine 完成其任务时,它应该调用 wg.Done() 来减少 WaitGroup 的计数器。
  3. Wait(): 阻塞调用它的 goroutine,直到 WaitGroup 的计数器变为零。这意味着所有通过 Add 方法添加的 goroutine 都已调用 Done() 完成任务。

正确使用 wg.Add() 的时机

理解 wg.Add() 的放置时机对于避免并发中的竞态条件至关重要。

示例代码:标准且正确的用法

以下是一个典型的 sync.WaitGroup 使用示例,它展示了如何正确地初始化 WaitGroup 并等待多个 goroutine 完成:

package main

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

// dosomething 模拟一个耗时操作,并在完成后调用 wg.Done()
func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration) // 模拟工作负载
    fmt.Println("Function in background, duration:", duration)
    wg.Done() // 任务完成后,通知 WaitGroup
}

func main() {
    var wg sync.WaitGroup // 声明一个 WaitGroup 变量

    // 在所有 go 语句之前,一次性设置需要等待的 goroutine 数量
    wg.Add(4) 

    // 启动四个 goroutine
    go dosomething(200, &wg)
    go dosomething(400, &wg)
    go dosomething(150, &wg)
    go dosomething(600, &wg)

    wg.Wait() // 阻塞主 goroutine,直到所有子 goroutine 完成
    fmt.Println("Done") // 所有任务完成后,打印 "Done"
}

输出结果 (顺序可能不同,但最终都会打印 "Done"):

BgSub
BgSub

免费的AI图片背景去除工具

下载
Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done

为何 wg.Add() 必须在 go 语句之前?

上述示例中,wg.Add(4) 发生在所有 go dosomething(...) 语句之前。这是 WaitGroup 正确工作的关键。如果 wg.Add() 发生在 go 语句之后,可能会引入竞态条件,导致程序行为不确定甚至崩溃。

  1. 竞态条件 (Race Condition) 的风险: 如果将 wg.Add(N) 放在 go 语句之后,或者更糟糕地,将 wg.Add(1) 放在被启动的 goroutine 内部,那么主 goroutine 有可能在子 goroutine 启动并调用 wg.Add() 之前就执行到 wg.Wait()。在这种情况下,WaitGroup 的计数器可能还未增加,wg.Wait() 会立即返回(因为计数器为零),而子 goroutine 仍在后台运行。这导致主程序过早结束,无法等待所有任务完成。

  2. WaitGroup 计数器低于零的恐慌 (Panic):WaitGroup 的计数器不能降到零以下。如果一个 goroutine 在 wg.Add() 增加计数器之前就调用了 wg.Done(),那么计数器会从零变为负数,这将导致程序发生 panic。例如,如果 wg.Add(1) 被放在 dosomething 函数内部,且该 goroutine 启动速度非常快,在主 goroutine 执行到 wg.Add(1) 之前就完成了任务并调用了 wg.Done(),就会出现这种情况。

  3. Go 内存模型的保证: Go 语言的内存模型提供了一些关于事件顺序的保证。其中一个重要的保证是:go 语句的执行(即启动一个新的 goroutine)发生在被启动的 goroutine 实际开始运行之前。这意味着,如果在 go 语句之前调用 wg.Add(),那么 wg.Add() 的操作一定会在新的 goroutine 开始执行其代码(包括 wg.Done())之前完成。这种顺序保证消除了竞态条件,确保 WaitGroup 的计数器在任何 Done() 操作发生之前都已正确增加。

wg.Add() 的灵活调用方式

虽然一次性调用 wg.Add(N) 是最常见且推荐的做法(当你知道需要等待的 goroutine 数量时),但在某些场景下,你也可以在每次启动一个 goroutine 前调用 wg.Add(1)。

func main() {
    var wg sync.WaitGroup

    // 每次启动一个 goroutine 前,增加计数器
    wg.Add(1) 
    go dosomething(200, &wg)

    wg.Add(1)
    go dosomething(400, &wg)

    wg.Add(1)
    go dosomething(150, &wg)

    wg.Add(1)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

这种逐个添加的方式在功能上是正确的,因为它同样保证了 wg.Add(1) 发生在对应的 go 语句之前。然而,当你知道确切的 goroutine 数量时,一次性调用 wg.Add(N) 更加简洁和高效。逐个添加的方式在循环中启动 goroutine 时可能更有用,例如:

func main() {
    var wg sync.WaitGroup
    durations := []time.Duration{200, 400, 150, 600}

    for _, d := range durations {
        wg.Add(1) // 每次迭代增加一个计数
        go dosomething(d, &wg)
    }

    wg.Wait()
    fmt.Println("Done")
}

注意事项与最佳实践

  • 指针传递 WaitGroup: 始终将 sync.WaitGroup 作为指针 (*sync.WaitGroup) 传递给函数,因为 WaitGroup 是一个值类型,如果按值传递,每个 goroutine 将获得 WaitGroup 的副本,导致同步失败。
  • 确保 Done() 被调用: 确保每个通过 Add() 增加计数器的 goroutine 最终都会调用 Done()。通常,这会在函数的 defer 语句中完成,以保证即使函数提前返回或发生错误,Done() 也能被调用。
    func dosomethingSafe(millisecs time.Duration, wg *sync.WaitGroup) {
        defer wg.Done() // 确保在函数退出时调用 Done()
        duration := millisecs * time.Millisecond
        time.Sleep(duration)
        fmt.Println("Function in background, duration:", duration)
        // 假设这里可能会有panic或者提前return
    }
  • 避免计数器归零以下: 如前所述,确保 Add() 总是先于 Done() 执行,以防止计数器变为负数导致 panic。

总结

sync.WaitGroup 是 Go 语言中实现并发任务同步的基石。正确地理解和使用 wg.Add()、wg.Done() 和 wg.Wait() 是编写健壮、无竞态条件的并发程序的关键。核心原则是:wg.Add() 必须在启动相应 goroutine 的 go 语句之前执行,以确保 WaitGroup 的计数器在任何 Done() 操作之前都被正确初始化。遵循这些最佳实践将有助于您高效地管理 Go 中的并发任务。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

401

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

543

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

53

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

197

2025.08.29

java值传递和引用传递有什么区别
java值传递和引用传递有什么区别

java值传递和引用传递的区别:1、基本数据类型的传递;2、对象的传递;3、修改引用指向的情况。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

108

2024.02.23

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

70

2026.01.23

c++空格相关教程合集
c++空格相关教程合集

本专题整合了c++空格相关教程,阅读专题下面的文章了解更多详细内容。

72

2026.01.23

yy漫画官方登录入口地址合集
yy漫画官方登录入口地址合集

本专题整合了yy漫画入口相关合集,阅读专题下面的文章了解更多详细内容。

297

2026.01.23

漫蛙最新入口地址汇总2026
漫蛙最新入口地址汇总2026

本专题整合了漫蛙最新入口地址大全,阅读专题下面的文章了解更多详细内容。

469

2026.01.23

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4.2万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

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

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