5.1 Goroutine 与调度模型


5.1 Goroutine 与调度模型

本节摘要:goroutine 是 Go 运行时调度的轻量执行流,初始栈仅数 KB、可随需增长,启动成本远低于操作系统线程。本节讲 go 关键字的用法与生命周期、主 goroutine 退出即全程序退出的规则、GMP 调度模型的直观图景、goroutine 与同步的初阶配合,以及泄漏的成因与检测思路。

先说结论

阅读完本节,你应当能够:

  1. 用 go 关键字正确启动并发执行流并传参;
  2. 解释主 goroutine 退出对子 goroutine 的影响;
  3. 描述 GMP 三层模型各角色分工;
  4. 说明 goroutine 相对线程的三个优势来源;
  5. 识别 goroutine 泄漏的典型成因。

一、并发不等于并行

先掰清两个词:并发是"同时管理多件事"的结构能力,并行是"同时真的在算"的硬件事实。一人炒三个菜是并发(轮流照看),三口锅三个厨师是并行。单核机器照样跑并发程序,靠的是快速切换制造"同时在办"的错觉。Go 的并发原语让你先写对结构,并行度交给运行时按核数自动铺开。

二、go 关键字:一个词的并发

func say(s string) { for i := 0; i < 3; i++ { fmt.Println(s, i) } } func main() { go say("world") // 立即返回,say在新的goroutine里跑 say("hello") // 主goroutine继续自己的活 }

go 后面跟函数调用,被调函数在新的执行流里运行,go 语句本身立刻返回。两条硬规则:

参数在 go 语句处求值并拷贝go say(msg) 里 msg 的当前值被带走,之后改 msg 不影响已启动的执行流。

主 goroutine 退出,程序就退出——不等其他 goroutine。上面例子如果 say hello 很快跑完,world 可能一行都没打印。这不是 bug 是设计:需要等待就必须显式同步(channel、WaitGroup,本章后续)。

⚠️ 常见坑(来自经典教学例子的演化):循环里 go func() { fmt.Println(i) }() 直接捕获循环变量 i,旧版本里所有 goroutine 共享同一个 i,输出可能全是最后一个值。Go 1.22 起每轮迭代是新变量,但显式传参 go func(n int) {...}(i) 永远是跨版本安全的写法。

三、GMP:三层调度模型

Go 运行时的调度器把三样东西分开管理:

  • G(goroutine):一个个待执行的任务,携带自己的栈与状态。
  • M(machine):操作系统线程,真正干活的引擎。
  • P(processor):逻辑处理器,持有可运行 G 的本地队列,M 必须绑定一个 P 才能执行 G。

几个直观结论:G 多 M 少,成千上万个 G 在少量线程上复用;某 G 陷入阻塞系统调用时,绑定的 M 一起堵,运行时会解绑 P 交给别的(或新的)M 继续跑队列里的 G,线程不会陪着闲死;某个 P 的队列空了会去别的 P 偷活干(工作窃取),负载自动均衡。GOMAXPROCS 环境变量控制 P 的数量,默认等于 CPU 核数。

四、为什么 goroutine 便宜

维度 操作系统线程 goroutine
初始栈 固定较大(MB级) 数 KB,按需增长收缩
创建切换 陷入内核,微秒级 用户态切换,纳秒级
数量级 数千已吃力 十万级常见
调度方 操作系统抢占 运行时协作加抢占结合

栈小是因为初始只给一点点,函数调用需要更多时运行时自动扩(拷贝式增长,第 3 章的逃逸分析与这里呼应)。切换快是因为不进内核。这两条合起来,让"每个连接一个 goroutine"的服务器模型在 Go 里是常态——写起来像同步阻塞,跑起来是高并发事件驱动。

五、泄漏:goroutine 的死亡方式

goroutine 结束只有三条路:函数跑完 return;panic 且未被 recover(整个程序崩);被运行时干掉(极少)。没有任何"从外面杀掉某个 goroutine"的 API。这是有意设计:粗暴外部终止会让资源清理与不变量维护无从谈起。想停,就得给协作式的退出信号——channel 关闭或 Context 取消(本章 5.3 与 5.5)。

泄漏的标准剧本:goroutine 在等一个永远没人发的 channel 消息,或等一个永远不会完成的 IO,调用方却早已返回。它不报错、不退出,只是白占几 KB 栈与一个调度位,累积起来内存与调度压力缓慢上涨。检测手段:运行时与指标库暴露的 goroutine 数量监控(突增曲线)、压测后抓 goroutine 堆栈转储(一个测试信号即可触发全量打印)。

💡 关键直觉:把 goroutine 当"合同工"管理——开工前就想好他的完工条件(谁通知他收工),没有退出条款的合同工就是泄漏。

本节速览

  • 并发是结构,并行是硬件;单核也跑并发。
  • go 参数即时求值拷贝;主 goroutine 退出即程序退出。
  • 循环变量捕获跨版本行为有差异,显式传参最稳。
  • GMP 分层:G 是任务、M 是线程、P 是持队列的逻辑处理器;阻塞时 P 会换 M 接管。
  • 便宜的两根支柱:小栈按需增长、用户态切换。
  • 无法外部杀死 goroutine,退出必须协作式:channel 或 Context。
  • 泄漏监控:goroutine 数指标加栈转储,是并发服务的标配仪表。

下一节讲 goroutine 之间怎么说话:channel。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U