1. Goroutine 基础启动与退出观察

Goroutine 基础启动与退出观察 学习卡片

为什么需要观察 goroutine 的启动与退出

Go 程序启动时,运行时环境会先创建一个主 goroutine 来执行 main 函数。当 main 函数返回时,整个程序立即结束,此时其他仍在运行的 goroutine 会被强制终止,不会等待它们完成。因此,观察多个 goroutine 的启动顺序、执行交错以及主 goroutine 退出对它们的影响,是理解 Go 并发模型的第一步。

示例程序:启动多个 goroutine 并观察执行

下面的程序启动 3 个 goroutine,每个 goroutine 打印自己的编号和当前时间戳,主 goroutine 在启动完成后短暂休眠再退出。通过观察输出,可以看到 goroutine 的执行顺序并不固定,且主 goroutine 退出后其他 goroutine 可能来不及完成。

package main

import (
	"fmt"
	"time"
)

func worker(id int) {
	for i := 0; i < 3; i++ {
		fmt.Printf("goroutine %d: 第 %d 次执行, 时间=%v\n", id, i, time.Now().UnixNano())
		time.Sleep(10 * time.Millisecond)
	}
	fmt.Printf("goroutine %d: 完成\n", id)
}

func main() {
	fmt.Println("main: 开始启动 goroutine")

	for i := 1; i <= 3; i++ {
		go worker(i)
	}

	fmt.Println("main: 3 个 goroutine 已启动")

	// 主 goroutine 休眠 25ms,观察子 goroutine 的执行情况
	time.Sleep(25 * time.Millisecond)

	fmt.Println("main: 主 goroutine 退出")
}

执行结果分析

运行上述程序,可能得到类似以下输出(每次运行结果不同):

main: 开始启动 goroutine
main: 3 个 goroutine 已启动
goroutine 1: 第 0 次执行, 时间=1700000000000000000
goroutine 3: 第 0 次执行, 时间=1700000000000100000
goroutine 2: 第 0 次执行, 时间=1700000000000200000
goroutine 1: 第 1 次执行, 时间=1700000000010000000
goroutine 3: 第 1 次执行, 时间=1700000000010100000
goroutine 2: 第 1 次执行, 时间=1700000000010200000
goroutine 1: 第 2 次执行, 时间=1700000000020000000
goroutine 3: 第 2 次执行, 时间=1700000000020100000
goroutine 2: 第 2 次执行, 时间=1700000000020200000
goroutine 1: 完成
goroutine 3: 完成
goroutine 2: 完成
main: 主 goroutine 退出

从输出中可以观察到几个关键现象:

  • 启动顺序不等于执行顺序:虽然按 1、2、3 的顺序启动,但实际打印顺序可能是 1、3、2 或其他排列,说明 goroutine 的调度由 Go 运行时决定,不保证顺序。
  • 执行交错:多个 goroutine 的输出会交替出现,说明它们在并发执行,而不是串行。
  • 主 goroutine 退出的影响:如果主 goroutine 的 time.Sleep 时间缩短到 5ms,那么子 goroutine 可能只执行了第 0 次就被强制终止,不会打印“完成”。
关键风险:主 goroutine 退出时,所有子 goroutine 立即被终止,不会执行 defer、不会完成循环、不会释放资源。因此在实际程序中,必须使用同步机制(如 sync.WaitGroup 或 channel)等待子 goroutine 完成,而不能依赖 time.Sleep 这种不可靠的方式。

goroutine 生命周期与主协程关系图解

下图展示了主 goroutine 与子 goroutine 的生命周期关系。主 goroutine 启动子 goroutine 后继续执行,子 goroutine 在后台并发运行;当主 goroutine 退出时,所有子 goroutine 被强制终止,无论它们是否完成。

主 goroutine 与子 goroutine 生命周期关系 时间 主 goroutine (main) 启动 go worker(1..3) Sleep(25ms) main 返回 goroutine 1 goroutine 2 goroutine 3 强制终止 完成 完成 完成 观察要点: 1. 子 goroutine 在 go 语句执行后立即开始并发运行,但具体调度顺序不确定。 2. 主 goroutine 的 Sleep 期间,子 goroutine 有机会执行;Sleep 结束后 main 返回。 3. main 返回时,所有未完成的子 goroutine 被强制终止,不会等待。

如何验证主 goroutine 退出的影响

将 time.Sleep(25 * time.Millisecond) 改为 time.Sleep(5 * time.Millisecond),重新运行程序。此时主 goroutine 很快退出,子 goroutine 可能只打印了第 0 次就被终止,输出中不会出现“完成”字样。这直接证明了主 goroutine 退出会强制结束所有子 goroutine。

实践建议:在编写并发程序时,不要用 time.Sleep 来“等待”goroutine 完成,因为无法确定需要等多久。正确的做法是使用 sync.WaitGroup 或 channel 进行同步,确保主 goroutine 在所有子 goroutine 完成后才退出。

关键结论

观察项现象原因
启动顺序按 1、2、3 启动go 语句按代码顺序执行
执行顺序不固定,可能交错Go 调度器决定,不保证顺序
主 goroutine 退出子 goroutine 立即终止main 返回时程序结束,不等待其他 goroutine
Sleep 时间不足子 goroutine 未完成主 goroutine 提前退出,强制终止子 goroutine