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

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

碧海醫心
发布: 2025-11-02 11:12:44
原创
256人浏览过

掌握 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"):

云雀语言模型
云雀语言模型

云雀是一款由字节跳动研发的语言模型,通过便捷的自然语言交互,能够高效的完成互动对话

云雀语言模型54
查看详情 云雀语言模型
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 中的并发任务。

以上就是掌握 Go 语言中的 sync.WaitGroup:并发任务的同步与管理的详细内容,更多请关注php中文网其它相关文章!

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

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

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

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