跳转至

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) 在前,打印消息在后:

  0 1 2 3 4 5 6 7 8 9
  We have called the function

顺序执行,天经地义4。

第二遍:加了 go——输出消失了。

go show_value(0)
fmt.Println("We have called the function")
  We have called the function
  ← 然后呢?没了。0 到 9 一个都没打出来。

书里把原因讲得很细:go 启动 goroutine 后,main 不等你,直接执行下一行; 打印完消息,main 到了函数末尾——所有 goroutine 被终止,show_value 还没来得及 打印第一个数5。

图说:go 是「撒手」的意思。撒手之后,main 是唯一的主心骨;主心骨一倒,全场熄灯。

第三遍:土法等待——Scanln。 书里用 fmt.Scanln()(等用户敲回车) 把 main 按在原地,趁这个空档让 goroutine 跑完:

  We have called the function 0
  1 2 3 … 9

这次数字全出来了,但方法很土:靠「等用户输入」拖时间,写不出真实程序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. 可带走的

  1. 并发 = 交替推进,并行 = 真的同时;单核也能并发(切换时间片);
  2. goroutine = go 关键字启动的函数调用,Go 运行时调度,不是操作系统线程;
  3. goroutine 的栈按需伸缩,成本远小于线程,可以开成千上万个;
  4. main 自己也是一个 goroutine;main 一返回,所有 goroutine 被杀;
  5. 普通调用会等函数返回;go 调用撒手就跑;
  6. GOMAXPROCS 决定同时用几个核,默认 = 核心数;
  7. WaitGroup 三件套:Add(开几个)、Done(交卷)、Wait(等清零),WaitGroup 要传指针;
  8. 多个 goroutine 的输出交错且顺序不保证;
  9. Scanln 拖时间是教学土法,真实程序用 WaitGroup;
  10. 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」)

  1. 出处:「Concurrency with Go」第 49–58 段(text/23-fm-learning-go-programming.txt:52,搜「context-switching」)。 ↩↩

  2. 出处:「Concurrency with Go」第 64 段(text/23-fm-learning-go-programming.txt:64,搜「lightweight threads」)。 ↩↩↩

  3. 出处:「Concurrency with Go」第 541 段(text/23-fm-learning-go-programming.txt:541,搜「number of cores」)。 ↩

  4. 出处:「Concurrency with Go」第 76–139 段(text/23-fm-learning-go-programming.txt:139,搜「We have called the function」)。 ↩

  5. 出处:「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」)。 ↩

  6. 出处:「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」)。 ↩

  7. 出处:「Concurrency with Go」第 391–536 段(text/23-fm-learning-go-programming.txt:481,搜「goroutine show_value1 100」)。交错的完整输出照原书。 ↩

  8. 出处:「Concurrency with Go」第 544–656 段(text/23-fm-learning-go-programming.txt:547,搜「synchronized mechanism」;第 550、553、556 段分别是 Add、Done、Wait 的定义)。 ↩

  9. 出处:「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 在最后,照原书。 ↩