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

Go 并发编程:深入理解 sync.WaitGroup 的正确使用与并发安全

霞舞
发布: 2025-11-02 11:59:27
原创
630人浏览过

Go 并发编程:深入理解 sync.WaitGroup 的正确使用与并发安全

本文深入探讨 go 语言中 `sync.waitgroup` 的正确使用方法,强调 `wg.add()` 必须在 `go` 语句之前调用的重要性,以避免竞态条件和程序崩溃。通过结合 go 内存模型,详细解释了 `add()` 和 `done()` 调用的时序保证,并提供了示例代码和最佳实践,帮助开发者编写健壮的并发程序。

在 Go 语言的并发编程中,`sync.WaitGroup` 是一个非常重要的同步原语,它允许我们等待一组 goroutine 完成其执行。它通过一个内部计数器来工作:`Add()` 方法增加计数器,`Done()` 方法减少计数器,而 `Wait()` 方法会阻塞,直到计数器归零。

`sync.WaitGroup` 的基本用法

以下是一个常见的 `sync.WaitGroup` 使用示例,展示了如何等待多个后台 goroutine 完成任务:

package main
<p>import (
"fmt"
"sync"
"time"
)</p><p>func dosomething(millisecs time.Duration, wg <em>sync.WaitGroup) {
duration := millisecs </em> time.Millisecond
time.Sleep(duration)
fmt.Println("Function in background, duration:", duration)
wg.Done() // 任务完成后调用 Done()
}</p><p>func main() {
var wg sync.WaitGroup
wg.Add(4) // 预先增加计数器,表示将启动4个 goroutine
go dosomething(200, &wg)
go dosomething(400, &wg)
go dosomething(150, &wg)
go dosomething(600, &wg)</p><pre class="brush:php;toolbar:false;">wg.Wait() // 等待所有 goroutine 完成
fmt.Println("Done")
登录后复制

}

上述代码的执行结果将是:

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
登录后复制

这个示例清晰地展示了 `wg.Add(4)` 在启动 goroutine 之前被调用,以及每个 goroutine 在完成任务后调用 `wg.Done()`。这种模式是 `sync.WaitGroup` 的正确且推荐用法。

立即进入豆包AI人工智官网入口”;

立即学习豆包AI人工智能在线问答入口”;

核心原则:`Add()` 的时机

使用 `sync.WaitGroup` 的一个关键原则是,wg.Add() 方法必须在对应的 go 语句之前被调用。这一点至关重要,它直接关系到程序的并发安全性和稳定性。

`WaitGroup` 的内部计数器从零开始。每次调用 `Add(delta int)`,计数器会增加 `delta`;每次调用 `Done()`,计数器会减少 1(相当于 `Add(-1)`)。当计数器降至零时,`Wait()` 方法才会解除阻塞。如果计数器在任何时候尝试降到零以下,`WaitGroup` 将会触发一个 panic。因此,确保 `Add()` 调用发生在 `Done()` 之前,是避免程序崩溃的关键。

并发安全与竞态条件

假设我们错误地将 `wg.Add()` 放在了 `go` 语句之后:

func main() {
    var wg sync.WaitGroup
    // 这是一个错误的示例,可能导致竞态条件甚至 panic
    go dosomething(200, &wg)
    wg.Add(1) // 如果 goroutine 启动并调用 Done() 发生在 Add() 之前,就会出问题
    // ... 其他 goroutine
    wg.Wait()
    fmt.Println("Done")
}
登录后复制

在这种情况下,会发生竞态条件。Go 调度器在启动 `go dosomething(...)` 后,可能在 `wg.Add(1)` 被执行之前,就调度 `dosomething` goroutine 运行。如果 `dosomething` 足够快,它可能会在 `wg.Add(1)` 执行之前调用 `wg.Done()`。此时,`WaitGroup` 的计数器仍然是 0,`Done()` 会尝试将其减为 -1,从而导致程序 panic。即使没有 panic,如果 `Wait()` 在 `Add()` 之前执行,也可能导致 `Wait()` 过早解除阻塞,而其他 goroutine 尚未完成。

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程483
查看详情 豆包AI编程

Go 内存模型解析

为了理解为什么 `Add()` 必须在 `go` 语句之前,我们需要参考 Go 内存模型。Go 内存模型定义了并发程序中事件的顺序,并提供了关于内存操作可见性的保证。

  • **单 Goroutine 内的顺序**:Go 内存模型保证,在一个单独的 goroutine 内部,程序的执行顺序与代码的书写顺序一致。
  • **Goroutine 的启动**:一个 `go` 语句在它所启动的 goroutine 开始执行之前完成。这意味着,`go` 语句之前的任何操作,都保证在新的 goroutine 开始执行其代码之前完成。

结合这两点,当我们将 `wg.Add()` 放在 `go` 语句之前时,我们得到了以下保证:

  1. `wg.Add()` 操作在主 goroutine 中执行。
  2. `go` 语句在主 goroutine 中执行,并启动一个新的 goroutine。
  3. 根据 Go 内存模型,`wg.Add()` 保证在 `go` 语句之前完成。
  4. 新的 goroutine 在 `go` 语句完成之后才开始执行。
  5. 新 goroutine 中的 `wg.Done()` 操作会在其内部逻辑执行完毕后调用。

因此,`wg.Add()` 保证在 `wg.Done()` 之前发生,从而避免了竞态条件和计数器下溢的风险。

`Add()` 的灵活调用方式

虽然一次性调用 `wg.Add(N)` 是高效且推荐的方式,尤其当已知 goroutine 数量时,但也可以选择在每次启动 goroutine 之前调用 `wg.Add(1)`。例如:

func main() {
    var wg sync.WaitGroup
    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)
<pre class="brush:php;toolbar:false;">wg.Wait()
fmt.Println("Done")
登录后复制

}

这种写法在功能上是正确的,因为它同样保证了每个 `Add(1)` 都发生在对应的 `go` 语句之前。然而,当 goroutine 数量已知时,一次性调用 `wg.Add(N)` 更加简洁和高效。如果 goroutine 的数量是动态的,那么在每次启动 goroutine 之前调用 `wg.Add(1)` 则是更合适的选择。

注意事项与最佳实践

  • **`Add()` 必须在 `go` 语句之前**:这是最核心的规则,务必遵守以确保并发安全。
  • **`WaitGroup` 传递指针**:在将 `WaitGroup` 传递给 goroutine 时,必须传递其指针 (`*sync.WaitGroup`),而不是值。否则,每个 goroutine 将操作 `WaitGroup` 的副本,导致同步失败。
  • **每个 `Done()` 对应一个 `Add()`**:确保 `Done()` 的调用次数与 `Add()` 增加的计数器值相匹配,否则 `Wait()` 可能永远阻塞或导致 panic。
  • **错误处理**:在实际应用中,goroutine 内部的错误处理也很重要。`WaitGroup` 仅负责同步 goroutine 的完成,不负责错误传递。可以结合 `context` 或通道来处理错误。

总结

`sync.WaitGroup` 是 Go 语言中实现并发同步的强大工具。正确理解和使用 `wg.Add()` 的时机是编写健壮、无竞态条件并发程序的关键。通过始终确保 `wg.Add()` 在 `go` 语句之前执行,并结合对 Go 内存模型的理解,开发者可以有效地协调 goroutine 的执行,从而构建高性能且可靠的 Go 应用程序。

以上就是Go 并发编程:深入理解 sync.WaitGroup 的正确使用与并发安全的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源: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号