文章目录
- 单进程时代
- 多进程/线程时代
- 协程时代
- 内核级线程模型(1:1)
- 用户级线程模型(N:1)
- 两级线程模型CMP(M:N)
- GM模型
- GMP模型
单进程时代
描述:每一个程序就是一个进程,直到程序进行完才行进行下一个进程。
问题:
- 单一的执行流程,计算机只能一个一个问题的处理
- 进程阻塞(比如说读取磁盘)带来的
CPU时间浪费
多进程/线程时代
描述:一个程序阻塞,CPU就切换到另一个程序执行
问题:
- 进程/线程占用内存高
- 进程/线程上下文
切换成本高
(数量越多,切换成本也越大) - 多进程/线程有 同步竞争问题(锁、资源冲突等),开发设计复杂
协程时代
描述:协程(用户态线程)绑定线程(内核态线程),CPU调度线程执行
内核级线程模型(1:1)
描述:1个用户线程对应1个内核线程,最容易实现,能够利用多核,一个线程被阻塞,不会阻塞其他线程,协程的调度都由操作系统完成。
问题:
- 上下文
切换成本高
- 创建、删除和切换都由操作系统完成
用户级线程模型(N:1)
描述:N个用户线程对应1个内核线程,上下文切换成本低,在用户态即可完成协程切换,N个协程轮询调度。
问题:
- 无法利用多核
- 一旦协程阻塞,造成
线程阻塞
,线程的其它协程无法执行
两级线程模型CMP(M:N)
描述:M个用户线程对应N个内核线程,上下文切换成本低,一个线程被阻塞,不会阻塞其他线程。
问题:
- 依赖库调度器的优化
GM模型
描述:内核线程去go协程队列(由锁保护)获取协程,执行,将协程放回队列最后。
问题:
- 创建、销毁、调度G都需要每个M获取锁,这就形成了
激烈的锁竞争
。 - M转移G(比如说M0执行G1的时候,G1创建了一个G2,G2被放进全局队列可能就会在M1执行,会
转移局部性差
)会造成延迟和额外的系统负载。 - 系统调用(CPU在M之间的切换)导致
频繁的线程阻塞
和取消阻塞操作增加了系统开销。
GMP模型
-
G(Goroutine)
代表Go协程Goroutine,存储了Goroutine的执行栈信息、Goroutine状态以及Goroutine的任务函数等。G的数量无限制,理论上只受内存的影响,创建一个G的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个Goroutine
,而且Go语言在G退出的时候还会把G清理之后放到P本地或者全局的闲置列表gFree中以便复用。 -
M(Machine)
Go对操作系统线程(OS thread)的封装,可以看作操作系统内核线程
,想要在CPU上执行代码必须有线程,通过系统调用clone创建。M在绑定有效的P后,进入一个调度循环,而调度循环的机制大致是从P的本地运行队列以及全局队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit 做清理工作并回到M,如此反复。M并不保留G状态,这是G可以跨M调度的基础。M的数量有限制,默认数量限制是10000,可以通过debug.SetMaxThreads()方法进行设置,如果有M空闲,那么就会回收或者睡眠。 -
P(Processor)
虚拟处理器,M执行G所需要的资源和上下文,只有将Р和M绑定
,才能让P中的G真正运行起来。P的数量决定了系统内最大可并行的G的数量,P的数量受本机的CPU核数影响,可通过环境变量$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU核心数。
- CPU感知不到Goroutine,只知道内核线程,所以需要Go调度器将协程调度到内核线程上面去,然后操作系统调度器将内核线程放到CPU上去执行,不会有频繁地的线程切换。
- 利用并行(由GOMAXPROCS限制CPU核数)
- 有本地队列,协程转移少
Work stealing
机制(如果本地队列不存在G,就从其他本地队列中偷取)- Go 1.14中实现了基于信号的"抢占式"调度,当系统时钟中断发生时,运行时会
发送一个信号
给目标goroutine,强制其主动让出CPU
。 Hand off
机制,不一直阻塞线程