为什么 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 句话

  1. 程序(Program)是静态代码,运行后成为进程(Process)。

  2. 进程负责资源管理,线程(Thread)负责执行任务。

  3. CPU 真正执行的是线程,而不是程序或 Goroutine。

  4. Goroutine 是 Go 创建的轻量执行任务,最终需要在线程上运行。

  5. GMP 通过 P 调度大量 Goroutine,让少量线程高效利用 CPU。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐