Go语言同步原语与数据竞争:WaitGroup

简介: 本文介绍了 Go 语言中 `sync.WaitGroup` 的使用方法和注意事项。作为同步原语,它通过计数器机制帮助等待多个 goroutine 完成任务。核心方法包括 `Add()`(设置等待数量)、`Done()`(减少计数)和 `Wait()`(阻塞直到计数归零)。文章详细讲解了其基本原理、典型用法(如等待 10 个 goroutine 执行完毕),并提供了代码示例。同时指出常见错误,例如 `Add()` 必须在 goroutine 启动前调用,以及 WaitGroup 不可重复使用。最后总结了适用场景和使用要点,强调避免竞态条件与变量捕获陷阱。

 

在Go语言并发编程中,我们经常需要等待多个 goroutine 执行完毕后再继续下一步操作。Go 提供的 sync.WaitGroup 就是专为这种**“等待一组任务完成”**而设计的同步原语。


一、基本原理

sync.WaitGroup 提供三个主要方法:

方法 说明
Add(n int) 设置等待的 goroutine 数量(加计数)
Done() 每个 goroutine 完成时调用(减计数)
Wait() 阻塞主 goroutine,直到计数归零

它内部使用计数器+条件变量,当所有 goroutine 都调用 Done() 后,Wait() 才会解除阻塞。


二、典型用法

示例:等待 10 个 goroutine 执行完毕

package main
import (
    "fmt"
    "sync"
)
func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 每个 goroutine 完成时调用
    fmt.Printf("Worker %d is working...\n", id)
    // 模拟工作
}
func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 10; i++ {
        wg.Add(1) // 每启动一个 goroutine,加1
        go worker(i, &wg)
    }
    wg.Wait() // 阻塞直到所有 goroutine 完成
    fmt.Println("All workers done.")
}

输出示意:

Worker 1 is working...
Worker 2 is working...
...
All workers done.

三、常见错误及注意事项

1. Add() 必须在 goroutine 启动前调用

错误示例:

go func() {
    wg.Add(1) // 此时 goroutine 可能已开始执行,竞态风险
    ...
}()

正确做法:

wg.Add(1)
go func() {
    ...
    wg.Done()
}()

2. 不可重复使用已完成的 WaitGroup(没有“重置”功能)

WaitGroup 设计为一次性同步器,不建议重复使用,若确需控制并发次数,可用 sync.Poolsemaphore 替代。


四、结合匿名函数使用

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        fmt.Println("i:", i)
    }(i)
}
wg.Wait()

⚠️ 注意:传参 i 必须显式传入闭包,避免捕获变量陷阱。


五、使用场景

  • • 等待一组任务执行完成(如:并发下载、批量计算)
  • • 控制主函数在 goroutine 完成后再退出
  • • 可搭配 Channel 和 Context 使用,实现更复杂的并发控制模型

六、小结

  • sync.WaitGroup 是 Go 并发编程中最常用的同步工具之一。
  • • 使用 Add/Done/Wait 实现多协程间的同步等待。
  • • 使用时避免竞态和变量捕获问题。

 

相关文章
|
3月前
|
SQL 数据库连接 Go
Go语言数据库编程:GORM 的基本使用
GORM 是 Go 语言最流行的 ORM 框架,封装了 database/sql,支持自动迁移、关联关系、事务等功能,开发体验接近高层语言的 ORM。本文介绍了 GORM 的安装与初始化、模型定义、自动迁移、基本 CRUD 操作、条件构造器、钩子函数、事务处理、日志调试等内容,帮助开发者快速掌握其使用方法。
|
1月前
|
存储 SQL 缓存
Java字符串处理:String、StringBuilder与StringBuffer
本文深入解析Java中String、StringBuilder和StringBuffer的核心区别与使用场景。涵盖字符串不可变性、常量池、intern方法、可变字符串构建器的扩容机制及线程安全实现。通过性能测试对比三者差异,并提供最佳实践与高频面试问题解析,助你掌握Java字符串处理精髓。
|
2月前
|
存储 JSON Go
Go语言实战案例-简单配置文件(INI格式)解析器
本案例讲解如何使用 Go 语言解析 INI 格式配置文件,适合入门学习文件读取与结构化数据处理。内容包括文件操作、字符串处理及多层 map 结构的应用,帮助开发者实现简易配置系统。
|
2月前
|
数据采集 JSON 自然语言处理
Go语言实战案例-统计文件中每个字母出现频率
《Go语言100个实战案例》中的“文件与IO操作篇 - 案例19”教你如何统计文本文件中每个英文字母的出现频率。通过实战练习,掌握文件读取、字符处理、map统计等基础技能,适合Go语言初学者提升编程能力。
|
1月前
|
安全 Go 开发者
Go语言实战案例:使用sync.Mutex实现资源加锁
在Go语言并发编程中,数据共享可能导致竞态条件,使用 `sync.Mutex` 可以有效避免这一问题。本文详细介绍了互斥锁的基本概念、加锁原理及实战应用,通过构建并发安全的计数器演示了加锁与未加锁的区别,并封装了一个线程安全的计数器结构。同时对比了Go中常见的同步机制,帮助开发者理解何时应使用 `Mutex` 及其注意事项。掌握 `Mutex` 是实现高效、安全并发编程的重要基础。
|
2月前
|
存储 算法 Go
Go语言实战案例-链表的实现与遍历
链表是一种经典的线性数据结构,通过节点指针连接实现灵活的插入与删除操作。本文使用 Go 语言实现单向链表,涵盖节点定义、链表操作(添加、删除、查找、遍历)及使用示例,帮助读者掌握链表基础与应用。
|
2月前
|
Go
Go语言实战案例-合并多个文本文件为一个
本案例来自《Go语言100个实战案例》,详细讲解如何使用Go语言合并多个文本文件。内容涵盖文件遍历、读取与写入操作,适合初学者学习文件处理技巧。案例包含完整代码示例和目录结构,帮助理解实际应用场景,如日志整合、数据汇总等。通过此案例,可掌握`filepath.Walk`、`os`、`bufio`等常用包的使用,并了解文件拼接顺序与内容处理方法。
|
3月前
|
安全 Linux
安装EPEL Repository Centos 7.9
记住,行走在Linux的世界,把“学习”作为你不可或缺的随身宝典。今天你学会了如何将EPEL这座外来的宝库接入你的系统,明天,你或许就能在这座宝库中发现一款能领你走向Linux大师之路的神器。
229 5
|
10月前
|
存储 运维 监控
项目中如何统一日志管理
在项目中统一日志管理,首要的是建立统一的日志标准、提供统一的日志服务、使用日志管理工具进行日志收集、分析和查询。
263 1
|
NoSQL 网络协议 Redis
Nomad 系列 -Nomad 网络模式
Nomad 系列 -Nomad 网络模式