
本文深入探讨 go 语言中 `sync.waitgroup` 的正确使用方法,强调 `wg.add()` 必须在 `go` 语句之前调用的重要性,以避免竞态条件和程序崩溃。通过结合 go 内存模型,详细解释了 `add()` 和 `done()` 调用的时序保证,并提供了示例代码和最佳实践,帮助开发者编写健壮的并发程序。
在 Go 语言的并发编程中,`sync.WaitGroup` 是一个非常重要的同步原语,它允许我们等待一组 goroutine 完成其执行。它通过一个内部计数器来工作:`Add()` 方法增加计数器,`Done()` 方法减少计数器,而 `Wait()` 方法会阻塞,直到计数器归零。
以下是一个常见的 `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人工智能在线问答入口”;
使用 `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 尚未完成。
为了理解为什么 `Add()` 必须在 `go` 语句之前,我们需要参考 Go 内存模型。Go 内存模型定义了并发程序中事件的顺序,并提供了关于内存操作可见性的保证。
结合这两点,当我们将 `wg.Add()` 放在 `go` 语句之前时,我们得到了以下保证:
因此,`wg.Add()` 保证在 `wg.Done()` 之前发生,从而避免了竞态条件和计数器下溢的风险。
虽然一次性调用 `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)` 则是更合适的选择。
`sync.WaitGroup` 是 Go 语言中实现并发同步的强大工具。正确理解和使用 `wg.Add()` 的时机是编写健壮、无竞态条件并发程序的关键。通过始终确保 `wg.Add()` 在 `go` 语句之前执行,并结合对 Go 内存模型的理解,开发者可以有效地协调 goroutine 的执行,从而构建高性能且可靠的 Go 应用程序。
以上就是Go 并发编程:深入理解 sync.WaitGroup 的正确使用与并发安全的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号