在了解go协程模型前,先简要的介绍下线程的实现模型。
线程实现模型
线程的实现模型主要包括三种:
- 用户级线程模型
- 内核级线程模型
- 两级线程模型
三种模型的主要区别是内核调度实体(KSE)与用户级线程的对应关系上。
KSE:操作系统内核的最小调度单元
用户级线程模型
KSE与用户线程是1:N的关系,即内核不区分进程内部的具体线程,而是将整个进程视为一个调度实体。
这种模型下:
- 由用户的调度器选择一个线程关联到KSE上,KSE只是单纯的执行者
- 非真正的并发,因为只会用到一个KSE(单个进程下)
- 进程内部的线程切换可以做的很轻,无需在内核态和用户态之间相互切换
内核级线程模型
KSE与用户线程是1:1的关系,每个线程都会由一个KSE拿去执行。
这种模型下:
- KSE与用户线程一一对应(当然超出KSE的线程需要等待)
- 真正的并发
- 需要频繁的在内核态和用户态之间进行切换
两级线程模型
KSE与用户线程是M:N的关系,每个线程可以独立的绑定到自己想绑定的KSE上,没有约束。
这种模型下:
- KSE与用户线程没有约束,每个KSE可以绑定多个用户线程
go 协程模型
go 是两级线程模型:
- go 调度器:实现 goroutine 到 kse的调度
- 内核调度器:实现 kse 到 cpu 的调度
GMP模型
go 是通过 G、M、P 模型实现二级线程模型的
G
- goroutine,一个可以独立运行的协程,go 调度器的核心就是在传送 G。
- 拥有栈、程序计数器、状态、工作函数,相当于待执行的 task
- 有多种状态的转化,可参考线程在操作系统中的状态机
M
- 操作系统的计算单元,与一个内核线程(KSE)绑定,代表真正的计算单元,由内核调度器调度和管理
- 拥有执行的状态信息(栈的起止位置,是否空闲等),以及与P的绑定关系,相当于真正的worker
P
- 逻辑处理器,是G与M中间缓冲带,是实现二级线程模型的关键
- 拥有任务队列G(局部、全局),执行上下文等,相当于dispatcher,决定是否将队列里的G交给当前关联的M执行
- 调度器会创建 runtime.GOMAXPROCS 个P,默认等于 CPU 数
从图中我们可以看出:
- M与P一一绑定,存在空闲M的场景(or M空闲的场景)
- 每个P都有一个局部的G队列,同时全局会有一个G队列
- 每个P都有一个正在运行的G
调度
- 每隔一段时间会从全局G队列中,取出可运行的G
- 从本地G队列中,取出可运行的G
- 阻塞等待可用的G
执行流程
-
将当前G绑定到M上
-
将当前G置为运行中
-
调用
gogo
,执行用户逻辑 -
结束时,调用
goexit0
,- 将G置为结束
- 清理G,解绑G与M
- 再次进入调度,从而实现了调度循环
内核态,用户态, 进程, 线程, 协程, 多路IO - 天使的魅影 - 博客园
The Go scheduler - Morsing's blog