goroutine:一个词开起的并发,和「main 不等任何人」¶
这一章讲 Go 最出名的能力:并发。 核心动作只有一个——在函数调用前写
go, 这个函数就「自己去跑」,主流程不等它。但主线例子会告诉你一个冷酷的事实: main 函数一返回,所有 goroutine 立刻被杀掉——不等它们跑完。 所以本章的真正主角是后半场的 sync.WaitGroup:让 main 规规矩矩等大家收工。
1. 先分清三个词:并发、并行、线程¶
书里给的三段定义,压缩成一张表1:
| 词 | 书里的话 | 大白话 |
|---|---|---|
| 并行(parallelism) | 多个任务真的同时跑,多核 CPU 各跑一个 | 两个人同时搬两箱货 |
| 并发(concurrency) | 多个任务交替推进,CPU 把时间片切给大家,切换快得像同时 | 一个人轮流搬两箱货,来回换 |
| 线程(thread) | 进程里的子任务,共享同一块内存,各自独立执行 | 一个程序里并行干活的「分身」 |
关键一句:单核机器上也能并发——靠的是上下文切换(context switching, 保存一个任务的现场、换另一个任务上 CPU);并行才需要多核1。
2. goroutine:轻到可以开成千上万的线程¶
goroutine 就是「用 go 关键字启动的函数调用」;它是 Go 运行时自己调度的 轻量执行单位,不是操作系统线程2。轻在哪,书里给了一个硬对比:
- 操作系统线程的栈是固定大小(好几 MB),开多了内存就吃不消;
- goroutine 的栈按需伸缩,启动成本小到可以开成千上万个2。
另外两条基本事实:
- 每个 Go 程序至少有一个 goroutine:main 函数自己就是(书里叫它 root goroutine);
- 一旦 main 函数走到尽头,所有 goroutine 不管跑到哪,全部被杀掉2。
还有一节专门讲 GOMAXPROCS:它决定 goroutine 最多同时用几个 CPU 核, 默认等于机器的核心数,启动时按操作系统检测3。一般用不到它, 知道「goroutine 会自动吃满多核」就够了。
3. 主线例子:同一个函数,跑四遍¶
本章主线,照原书用 show_value(0)(打印 0 到 9)走四个版本,
每一个版本解决上一个版本暴露的问题。
第一遍:普通调用。 show_value(0) 在前,打印消息在后:
顺序执行,天经地义4。
第二遍:加了 go——输出消失了。
书里把原因讲得很细:go 启动 goroutine 后,main 不等你,直接执行下一行; 打印完消息,main 到了函数末尾——所有 goroutine 被终止,show_value 还没来得及 打印第一个数5。
图说:go 是「撒手」的意思。撒手之后,main 是唯一的主心骨;主心骨一倒,全场熄灯。
第三遍:土法等待——Scanln。 书里用 fmt.Scanln()(等用户敲回车)
把 main 按在原地,趁这个空档让 goroutine 跑完:
这次数字全出来了,但方法很土:靠「等用户输入」拖时间,写不出真实程序6。
第四遍:两个 goroutine 并行交错。 同时开两个:go show_value(0) 和
go show_value1(100)(打印 100 到 109),各睡 200 毫秒再打下个数。
输出是交错的:100, 0, 1, 101, 102, 3, 103…7——
图说:两个 goroutine 轮流上 CPU,你一段我一段。 交错的顺序每次运行都可能不同; 谁也没法预言下一个数是谁的——这就是并发程序的常态,也正是它难写的原因。
4. 正解:sync.WaitGroup 三件套¶
WaitGroup 是标准库 sync 包里的一个计数器:main 数着「还有几个 goroutine 没交卷」, 全部交完才放行。 三个方法,书里各一句话8:
| 方法 | 谁调 | 干什么 |
|---|---|---|
Add(n) |
main | 开工前:我要等 n 个 |
Done() |
每个 goroutine | 交卷时:我完事了(计数减一) |
Wait() |
main | 原地等,直到计数归零 |
func show_value(i int, wg *sync.WaitGroup) {
for i < 10 {
fmt.Println(i)
time.Sleep(200 * time.Millisecond)
i++
}
wg.Done() // 交卷
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // 等两个
go show_value(0, &wg)
fmt.Println("We have called a goroutine show_value")
go show_value1(100, &wg)
fmt.Println("We have called a goroutine show_value1")
wg.Wait() // 数到零才放行
fmt.Println("Done") // 这一行保证最后打
}
走一遍:main 说「等 2 个」→ 两个 goroutine 交错打印 → 各自 Done 一次 →
计数归零 → Wait() 放行 → 打出 Done9。
两个细节书里演示到位:
- WaitGroup 必须传指针(
wg *sync.WaitGroup):传值就复制了计数器, main 等的是副本,永远等不到零(呼应拆解 09 的传指针改值); - 输出里
Done稳稳在最后——这是 Scanln 土法给不了的保证。
判断(我们的,不是书里的): WaitGroup 是 Go 并发的「胶水」, 但它只解决「等不等」的问题,不解决「抢不抢」的问题—— 两个 goroutine 改同一个变量,WaitGroup 管不着,结果照样错。 所以本书的顺序是对的:先学会「怎么等」(本章),再学「怎么不抢」(下一章的 channel 和 mutex)。 如果错,会错在: Go 运行时对「main 不等任何人」的裁量比别的语言都狠 (Java 的守护线程类似但更隐晦),新手第一个并发程序十有八九栽在这里—— 我们把它当成铁律写,正是为了让你不栽。
5. 作者的判断与证据¶
| 书里的说法 | 性质 |
|---|---|
| 「goroutine 是轻量级线程、栈按需伸缩」 | 机制事实,属实 |
| 「并发通过快速切换看起来像同时」 | 教科书口径,属实 |
| 「GOMAXPROCS 默认等于核心数」 | 属实 |
6. 边界与局限¶
- 没有讲 goroutine 泄露(goroutine 永远阻塞、Done 永远不调,Wait 卡死)—— WaitGroup 用错了最常见的死法(补充,不在书里);
- 没有讲 time.Sleep 之外的计时工具(Ticker/Timer),Sleep 只是教学用的节拍器;
- 交错输出的「顺序不保证」本书靠观察现象带过,没有讲调度器的任何机制—— 对入门是正确取舍,但也意味着:并发程序的结果不能靠「我跑了三次都对」验证;
- sync.Once、errgroup 等 WaitGroup 的实战变体未出现。
7. 可带走的¶
- 并发 = 交替推进,并行 = 真的同时;单核也能并发(切换时间片);
- goroutine = go 关键字启动的函数调用,Go 运行时调度,不是操作系统线程;
- goroutine 的栈按需伸缩,成本远小于线程,可以开成千上万个;
- main 自己也是一个 goroutine;main 一返回,所有 goroutine 被杀;
- 普通调用会等函数返回;
go调用撒手就跑; - GOMAXPROCS 决定同时用几个核,默认 = 核心数;
- WaitGroup 三件套:Add(开几个)、Done(交卷)、Wait(等清零),WaitGroup 要传指针;
- 多个 goroutine 的输出交错且顺序不保证;
- Scanln 拖时间是教学土法,真实程序用 WaitGroup;
- WaitGroup 管「等不等」,不管「抢不抢」——抢的问题下一章解决。
8. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 并发的作用 | Concurrency with Go | text/23-fm-learning-go-programming.txt:46(搜「overlapping period」) |
| 并发 vs 并行 | Concurrency with Go | text/23-fm-learning-go-programming.txt:52(搜「context-switching」) |
| 线程定义 | Concurrency with Go | text/23-fm-learning-go-programming.txt:58(搜「sub-task of a process」) |
| goroutine 定义、main 是 root、main 退出杀全部 | Concurrency with Go | text/23-fm-learning-go-programming.txt:64(搜「lightweight threads」) · text/23-fm-learning-go-programming.txt:190(搜「all Goroutines are terminated」) |
| 普通调用 vs go 调用 | Concurrency with Go | text/23-fm-learning-go-programming.txt:139(搜「We have called the function」) · text/23-fm-learning-go-programming.txt:190(搜「only message」) |
| Scanln 土法 | Concurrency with Go | text/23-fm-learning-go-programming.txt:229(搜「Scanln」) · text/23-fm-learning-go-programming.txt:238(搜「We have called the function 0」) |
| 混合调用与交错输出 | Concurrency with Go | text/23-fm-learning-go-programming.txt:328(搜「We have called a goroutine 0」) |
| 两个 goroutine 交错 | Concurrency with Go | text/23-fm-learning-go-programming.txt:481(搜「We have called a goroutine show_value We have called a goroutine show_value1 100」) |
| GOMAXPROCS | Concurrency with Go | text/23-fm-learning-go-programming.txt:541(搜「number of cores」) |
| WaitGroup 三方法与示例 | Concurrency with Go | text/23-fm-learning-go-programming.txt:547(搜「synchronized mechanism」) · text/23-fm-learning-go-programming.txt:637(搜「wg.Add(2)」) · text/23-fm-learning-go-programming.txt:652(搜「wg.Wait()」) · text/23-fm-learning-go-programming.txt:727(搜「Done」) |
-
出处:「Concurrency with Go」第 49–58 段(
text/23-fm-learning-go-programming.txt:52,搜「context-switching」)。 ↩↩ -
出处:「Concurrency with Go」第 64 段(
text/23-fm-learning-go-programming.txt:64,搜「lightweight threads」)。 ↩↩↩ -
出处:「Concurrency with Go」第 541 段(
text/23-fm-learning-go-programming.txt:541,搜「number of cores」)。 ↩ -
出处:「Concurrency with Go」第 76–139 段(
text/23-fm-learning-go-programming.txt:139,搜「We have called the function」)。 ↩ -
出处:「Concurrency with Go」第 175–190 段(
text/23-fm-learning-go-programming.txt:190,搜「only message」;text/23-fm-learning-go-programming.txt:190,搜「all Goroutines are terminated」)。 ↩ -
出处:「Concurrency with Go」第 223–265 段(
text/23-fm-learning-go-programming.txt:229,搜「Scanln」;text/23-fm-learning-go-programming.txt:238,搜「We have called the function 0」)。 ↩ -
出处:「Concurrency with Go」第 391–536 段(
text/23-fm-learning-go-programming.txt:481,搜「goroutine show_value1 100」)。交错的完整输出照原书。 ↩ -
出处:「Concurrency with Go」第 544–656 段(
text/23-fm-learning-go-programming.txt:547,搜「synchronized mechanism」;第 550、553、556 段分别是 Add、Done、Wait 的定义)。 ↩ -
出处:「Concurrency with Go」第 580–727 段(
text/23-fm-learning-go-programming.txt:637,搜「wg.Add(2)」;text/23-fm-learning-go-programming.txt:652,搜「wg.Wait()」;text/23-fm-learning-go-programming.txt:727,搜「Done」)。输出中 Done 在最后,照原书。 ↩