Menu
CoddyTech

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 中构建线程安全类型至关重要。

challenge icon

挑战

简单

我们来构建一个下载管理器,使用 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"
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器