sync.WaitGroup
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 64 课,共 107 课。
在 Goroutines Basics 这节课中,我们使用 time.Sleep 等待 goroutine 完成,这种方法并不可靠。sync.WaitGroup 提供了一种恰当的方式,用于等待一组 goroutine 完成工作。
WaitGroup 的作用类似计数器。启动 goroutine 前将计数加 1,goroutine 结束时将计数减 1,并阻塞等待计数归零:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 计数器加 1
go func(id int) {
defer wg.Done() // 完成后计数器减 1
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // 阻塞,直到计数器为 0
fmt.Println("All workers complete")
}三个关键方法是:使用 Add(n) 将计数器增加 n,使用 Done() 将计数器减一(等同于 Add(-1)),以及使用 Wait() 阻塞,直到计数器变为零。使用 defer wg.Done() 可确保即使 goroutine 发生 panic,计数器也会递减。
一个常见错误是在 goroutine 内部调用 Add,而不是在启动它之前调用。这会造成竞态条件,导致 Wait 可能在所有 goroutine 都注册之前就返回:
// 错误:存在竞态条件
go func() {
wg.Add(1) // 可能在 Wait() 之后运行
defer wg.Done()
}()
// 正确:启动前调用 Add
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups 通常嵌入结构体中,用于协调并发操作,因此它们对于在 Go 中构建线程安全类型至关重要。
挑战
简单我们来构建一个下载管理器,使用 sync.WaitGroup 协调多个并发文件下载。管理器会跟踪所有下载何时完成,而不依赖任意设置的休眠计时器。
你需要将代码分成两个文件:
downloader.go:定义下载协调逻辑。创建一个
Download结构体,包含ID(int)、Filename(string)和Size(int,表示 KB)字段。为
Download实现方法Process(wg *sync.WaitGroup),通过根据文件大小休眠一段时间来模拟下载(使用Size * 10毫秒)。下载完成时,打印:Downloaded: [Filename] ([Size]KB)。使用defer wg.Done()确保 WaitGroup 计数器正确递减。创建函数
StartDownloads(downloads []Download) int,使用 goroutine 并发启动所有下载,并等待它们全部完成。记得在启动每个 goroutine 之前调用wg.Add(1),不要在 goroutine 内调用。返回所有下载的总大小。main.go:读取下载信息并协调并发下载。读取下载数量,然后逐个读取每个下载的 ID、文件名和大小。创建下载项,并将它们传给
StartDownloads。所有下载完成后,打印:All downloads complete: [total]KB total
输入如下:
- 第 1 行:下载数量(整数)
- 后续各行:每个下载占三行,依次为 ID(整数)、文件名(字符串)和大小(单位为 KB 的整数)
例如,给定:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10输出应显示各项下载完成的情况(文件越小,休眠时间越短,因此完成得越早),然后显示汇总:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB total与使用 time.Sleep 相比,关键区别在于 wg.Wait() 会阻塞,直到所有 goroutine 都通过 Done() 发出完成信号。无论每次下载需要多长时间,这都能让你实现精确同步。
亲手试试
package main
import (
"fmt"
)
func main() {
// 读取下载数量
var n int
fmt.Scanln(&n)
// 读取下载信息
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO:用 downloads 切片调用 StartDownloads
// TODO:输出汇总信息:"All downloads complete: [total]KB total"
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器