0

0

Go语言协程同步:使用 sync.WaitGroup 的最佳实践

霞舞

霞舞

发布时间:2025-11-10 13:26:31

|

795人浏览过

|

来源于php中文网

原创

Go语言协程同步:使用 sync.WaitGroup 的最佳实践

本文深入探讨了在go语言中,当启动多个goroutine并行处理任务时,如何优雅且高效地等待所有goroutine完成其工作。我们将重点介绍并演示 `sync.waitgroup` 这一标准库提供的机制,它是实现此类并发同步的惯用且推荐方式,相比于手动管理通道,`waitgroup` 提供了更简洁、健壮的解决方案。

在Go语言的并发编程中,我们经常会遇到需要并行处理大量数据或执行耗时操作的场景。通过启动多个goroutine来并发执行这些任务,可以显著提高程序的性能。然而,一个常见的挑战是,主goroutine需要在所有子goroutine完成其工作后才能继续执行后续逻辑,例如汇总结果或释放资源。如果主goroutine不等候,它可能会提前退出,导致部分并发任务未能完成,或者后续操作依赖于未就绪的数据。

一种直观但并非最惯用的同步方式是使用通道(channel)。例如,为每个goroutine分配一个写入通道的权限,并在它们完成时向通道发送一个信号,主goroutine则通过循环接收这些信号来判断所有任务是否完成。虽然这种方法可行,但对于简单的“等待所有任务完成”的场景,它显得有些冗余和复杂。Go标准库提供了更为简洁和高效的 sync.WaitGroup 类型来专门处理这类同步需求。

解决方案:使用 sync.WaitGroup

sync.WaitGroup 是Go语言中专门用于等待一组goroutine完成的同步原语。它维护一个内部计数器,通过 Add 方法增加计数,Done 方法减少计数,以及 Wait 方法阻塞直到计数器归零。

sync.WaitGroup 工作原理详解

sync.WaitGroup 提供了三个核心方法:

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

晓象AI资讯阅读神器
晓象AI资讯阅读神器

晓象-AI时代的资讯阅读神器

下载
  1. Add(delta int): 用于增加 WaitGroup 的内部计数器。通常在启动新的goroutine之前调用,delta 参数表示要增加的计数。例如,wg.Add(1) 表示增加一个需要等待的goroutine。
  2. Done(): 用于减少 WaitGroup 的内部计数器。通常在每个goroutine完成其工作时调用,表示该goroutine已完成。
  3. Wait(): 阻塞当前goroutine,直到 WaitGroup 的内部计数器归零。这意味着所有通过 Add 方法增加的goroutine都已通过 Done 方法完成。

实践示例

假设我们有一个 Huge 函数,需要对一个切片 lst 中的每个 foo 元素执行一个耗时操作 performSlow。我们希望并发地执行 performSlow,并在所有操作完成后,再调用 someValue 函数。

package main

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

// 假设这是一个耗时操作
func performSlow(item string) {
    fmt.Printf("开始处理: %s\n", item)
    time.Sleep(time.Second) // 模拟耗时操作
    fmt.Printf("完成处理: %s\n", item)
}

// 定义一个示例类型
type foo string

func Huge(lst []foo) string {
    var wg sync.WaitGroup // 声明一个 WaitGroup 变量

    for _, item := range lst {
        wg.Add(1) // 每启动一个goroutine,计数器加1
        // 使用匿名函数包裹 performSlow,以便在goroutine内部调用 wg.Done()
        go func(data foo) {
            defer wg.Done() // 确保无论如何,goroutine结束时都会调用 Done()
            performSlow(string(data))
        }(item) // 将 item 作为参数传递给匿名函数,避免闭包陷阱
    }

    wg.Wait() // 阻塞,直到所有goroutine都调用了 Done(),计数器归零

    fmt.Println("所有并发任务已完成。")
    return someValue(lst) // 所有任务完成后,执行后续逻辑
}

// 假设这是需要所有并发任务完成后才能调用的函数
func someValue(lst []foo) string {
    return fmt.Sprintf("基于 %v 的最终结果。", lst)
}

func main() {
    items := []foo{"A", "B", "C", "D"}
    result := Huge(items)
    fmt.Println(result)
}

代码解释:

  1. var wg sync.WaitGroup: 在 Huge 函数内部声明一个 WaitGroup 实例。
  2. wg.Add(1): 在 for 循环中,每次启动一个新的goroutine之前,我们都会调用 wg.Add(1)。这会增加 WaitGroup 的内部计数器,表示有一个新的任务需要等待完成。
  3. go func(data foo) { ... }(item): 我们为每个切片元素启动一个独立的goroutine。注意这里使用了匿名函数并传入 item 作为参数 data,这是为了避免Go闭包在循环中捕获循环变量的常见陷阱。
  4. defer wg.Done(): 这是 WaitGroup 的最佳实践。在每个goroutine内部,我们使用 defer 关键字确保 wg.Done() 在 performSlow 函数执行完毕(无论成功还是发生panic)后被调用。这会将 WaitGroup 的计数器减1。
  5. wg.Wait(): 在 for 循环结束后,主goroutine调用 wg.Wait()。这个调用会阻塞主goroutine,直到 WaitGroup 的内部计数器变为零。一旦计数器归零,Wait() 方法返回,主goroutine才能继续执行,确保了 someValue(lst) 在所有并发操作完成后才被调用。

为何 sync.WaitGroup 是更优选择

  • 简洁性与可读性: 相比于手动创建和管理通道来同步,WaitGroup 的API更加简洁明了,直接表达了“等待一组任务完成”的意图。
  • 效率: WaitGroup 专为这种场景设计,其内部实现通常比基于通道的通用通信机制更高效,因为它避免了不必要的通道操作和上下文切换。
  • 惯用: sync.WaitGroup 是Go语言社区公认的、用于此类同步问题的标准和惯用方式。

使用注意事项与最佳实践

  • Add 必须在 go func() 之前调用: 确保 WaitGroup 的计数器在goroutine开始执行之前就已经增加。如果在goroutine内部调用 Add,可能会出现竞态条件,导致 Wait() 在所有 Add 调用完成之前就返回。
  • 使用 defer wg.Done(): 强烈建议在goroutine的开头使用 defer wg.Done()。这确保了即使goroutine因错误或panic而提前退出,Done() 也会被调用,避免 WaitGroup 永远无法归零,导致 Wait() 永久阻塞。
  • 避免闭包陷阱: 在循环中启动goroutine时,如果goroutine内部需要使用循环变量,应将其作为参数传递给匿名函数,以避免所有goroutine最终都引用同一个(循环结束时的)变量值。如示例中的 go func(data foo) { ... }(item)。
  • WaitGroup 的生命周期: WaitGroup 实例通常在需要同步的函数内部创建和使用。如果需要在多个函数之间共享 WaitGroup,应通过指针传递 *sync.WaitGroup。

总结

sync.WaitGroup 是Go语言中实现并发同步的强大工具,特别适用于“等待所有并发任务完成”的场景。通过合理地使用 Add、Done 和 Wait 方法,开发者可以构建出高效、健壮且易于理解的并发程序。掌握 sync.WaitGroup 是Go语言并发编程中不可或缺的一项技能,它能帮助我们编写出更符合Go语言哲学的高质量代码。

相关专题

更多
string转int
string转int

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

318

2023.08.02

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

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

538

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

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

234

2023.09.06

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

445

2023.09.25

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

248

2023.10.13

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

698

2023.10.26

PS使用蒙版相关教程
PS使用蒙版相关教程

本专题整合了ps使用蒙版相关教程,阅读专题下面的文章了解更多详细内容。

23

2026.01.19

热门下载

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

精品课程

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

共32课时 | 3.9万人学习

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号