为什么 Go 可以同时运行成千上万个 Goroutine?从程序执行到 GMP 调度模型
为什么 Go 可以同时运行成千上万个 Goroutine?从程序执行到 GMP 调度模型
很多 Go 初学者第一次接触 Goroutine 时,都会产生一个疑问:
一个 Goroutine 到底是什么?它和线程有什么区别?
我们经常看到:
G → P → M → CPU
但是很多文章直接告诉你:
- G 是 Goroutine
- P 是 Processor
- M 是 Machine
然后结束。
结果就是:
知道每个字母代表什么,却不知道为什么 Go 要这样设计。
真正理解 GMP,需要先理解几个基础问题:
- 程序为什么能够被 CPU 执行?
- 线程到底是什么?
- 为什么不能一个 Goroutine 对应一个线程?
- 为什么 Go 需要自己实现一套调度系统?
一、程序(Program)是什么?
程序(Program)指的是:
存储在硬盘中的代码文件。
例如:
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
编译之后,会生成:
hello.exe
这个 hello.exe 就是程序。
程序本质上是一堆机器指令。
例如:
101010101010...
但是程序本身不会运行。
它只是存放在硬盘中的代码。
可以理解为:
程序是一份任务说明书。
说明书不会自己完成任务,程序也不会自己执行。
它需要操作系统加载。
二、程序运行后变成什么?
当我们双击:
hello.exe
操作系统收到请求:
“我要运行这个程序。”
于是操作系统会创建:
进程(Process)
关系:
程序(Program)
↓ 运行
进程(Process)
二者区别:
| 程序 | 进程 | |
|---|---|---|
| 英文 | Program | Process |
| 位置 | 硬盘 | 内存 |
| 状态 | 静态代码 | 正在运行 |
| 是否占用资源 | 否 | 是 |
例如:
硬盘中:
chrome.exe
运行后:
Chrome进程
三、进程为什么还需要线程?
很多人第一次学习操作系统时会疑惑:
既然已经有进程了,为什么还需要线程?
原因是:
进程负责管理资源,但不负责真正执行代码。
真正执行代码的是:
线程(Thread)
因此:
进程
↓
线程
↓
CPU执行
例如浏览器:
一个 Chrome 进程里面可能有:
Chrome进程
├── 页面渲染线程
├── 网络请求线程
├── JavaScript线程
└── 音视频线程
不同线程负责不同任务。
四、线程(Thread)到底是什么?
这是理解 GMP 最重要的一步。
很多人会认为:
线程是不是一个装代码的容器?
不是。
线程不是代码。
线程也不是程序。
线程更准确的定义:
线程是 CPU 执行程序时的一条执行路径。
例如:
func main(){
A()
B()
C()
}
CPU执行:
A()
↓
B()
↓
C()
但是如果程序暂停:
CPU需要知道:
- 执行到了哪里?
- 当前变量是什么?
- 下一步执行什么?
这些信息就是:
上下文(Context)
线程保存的就是这些执行状态。
包括:
- 程序计数器(Program Counter)
- 寄存器状态(Register)
- 栈信息(Stack)
- 当前执行位置
所以:
线程不是“装代码”。
而是:
记录代码执行状态,并提供 CPU 执行路线。
五、CPU、进程、线程之间到底是什么关系?
正确关系:
程序 Program
↓
进程 Process
↓
线程 Thread
↓
CPU执行
注意:
线程不属于 CPU。
很多人会误解:
CPU执行线程,所以线程属于CPU。
实际上:
CPU只是执行者。
可以类比:
- 公司 = 进程
- 员工 = 线程
- 工具 = CPU
员工属于公司。
员工使用工具完成工作。
所以:
进程拥有线程,线程被 CPU 执行。
六、为什么需要多个线程?
假设一个程序只有一个线程:
下载文件
↓
下载完成
↓
播放视频
↓
响应鼠标
所有事情必须排队。
用户体验会很差。
所以:
一个进程可以创建多个线程:
进程
├── 下载线程
├── 播放线程
└── 输入线程
CPU 可以快速切换这些线程。
用户感觉多个任务同时运行。
七、什么是内核级线程?
在理解内核级线程之前,需要先知道:
内核(Kernel)是什么?
操作系统可以简单分成两部分:
用户空间
应用程序:
Go
浏览器
QQ
----------------
内核空间
操作系统:
管理CPU
管理内存
管理线程
----------------
硬件:
CPU
内存
磁盘
内核级线程(Kernel Thread)
内核级线程:
就是:
由操作系统内核管理的线程。
例如:
程序创建线程:
程序
↓
操作系统内核
↓
创建线程
↓
CPU执行
线程的:
- 创建
- 调度
- 切换
- 销毁
都由操作系统负责。
八、什么是用户级?
用户级:
英文:
User Level
意思:
由应用程序自己管理。
例如:
Go Runtime 创建:
G1
G2
G3
操作系统并不知道这些 Goroutine。
操作系统只知道:
线程1
线程2
不知道:
G1
G2
G3
所以:
Goroutine 属于用户级调度。
九、为什么 Go 不直接使用线程?
因为线程比较重。
创建线程需要:
- 操作系统参与
- 分配资源
- 保存线程状态
如果服务器:
同时处理:
100000个请求
如果:
一个请求创建一个线程:
100000个线程
会产生巨大压力:
- 内存占用增加
- 线程切换成本高
- 操作系统调度压力大
所以 Go 设计了:
Goroutine
十、什么是 Goroutine?
Goroutine:
是 Go Runtime 创建的:
用户级轻量执行单元。
例如:
go task()
Go 创建:
G1
注意:
Goroutine 不是线程。
区别:
| 线程 | Goroutine | |
|---|---|---|
| 英文 | Thread | Goroutine |
| 管理者 | 操作系统 | Go Runtime |
| 数量 | 较少 | 可以非常多 |
| 切换成本 | 高 | 低 |
十一、为什么 Goroutine 最终还是需要线程?
因为:
CPU 不认识 Goroutine。
CPU 只认识:
线程(Thread)
所以:
Goroutine 最终必须运行在线程上。
关系:
Goroutine
↓
线程
↓
CPU
但是:
不是:
一个G对应一个线程
而是:
多个G
↓
少量线程
例如:
G1
G2
G3
G4
↓
M1
十二、一个线程为什么可以执行多个 Goroutine?
关键:
Goroutine 也保存自己的执行状态。
例如:
G1:
执行到第20行
G2:
执行到第50行
当 Go 切换任务:
保存:
G1上下文
加载:
G2上下文
然后继续运行。
注意:
线程没有改变。
仍然是:
M1
只是:
当前运行的 Goroutine 改变了。
所以:
不是:
线程装载多个协程。
而是:
一个线程可以不断切换执行多个 Goroutine。
十三、为什么还需要 P?
如果直接:
G → M
会有什么问题?
假设:
10000个G
4个M
那么问题来了:
谁决定:
哪个 G 分配给哪个 M?
如果没有调度管理:
大量 Goroutine 会产生混乱。
所以 Go 增加了:
P(Processor)
十四、P(Processor)是什么?
P:
不是 CPU。
也不是线程。
P 是:
Go Runtime 中负责调度 Goroutine 的资源。
它负责:
- 管理 Goroutine 队列
- 分配 Goroutine
- 协调 G 和 M
关系:
G
↓
P
↓
M
↓
CPU
十五、GMP 三个角色分别是什么?
G(Goroutine)
表示:
要执行的任务。
例如:
go func(){
}()
产生一个 G。
P(Processor)
表示:
调度管理者。
负责:
安排哪个 G 运行。
M(Machine)
表示:
操作系统线程。
负责:
真正执行代码。
十六、一次 Goroutine 执行过程
例如:
go task()
执行过程:
创建G
↓
G进入P管理的队列
↓
P选择合适的M
↓
M加载G的执行状态
↓
CPU执行
完整模型:
CPU
↑
M
(操作系统线程)
↑
P
(Go调度器)
↑
G
(任务)
十七、最终理解 GMP
一句话:
GMP 是 Go 为了解决大量并发任务与有限线程之间矛盾而设计的一套调度模型。
其中:
G:
我要执行的任务
P:
负责安排任务
M:
真正执行任务
最终流程:
G 找 P
P 找 M
M 上 CPU
总结:理解 GMP 必须记住的 5 句话
-
程序(Program)是静态代码,运行后成为进程(Process)。
-
进程负责资源管理,线程(Thread)负责执行任务。
-
CPU 真正执行的是线程,而不是程序或 Goroutine。
-
Goroutine 是 Go 创建的轻量执行任务,最终需要在线程上运行。
-
GMP 通过 P 调度大量 Goroutine,让少量线程高效利用 CPU。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)