G(Goroutine):协程,用户级的轻量级线程。
M:对内核线程的封装
P:为G和M的调度对象,主要用途是用来执行goroutine,维护了一个goroutine队列,即runqueue
由来
单进程时代
这个时代不需要调度器,早起的操作系统每个程序就是一个进程,直到一个程序运行完,才能进入下一个进程,一切的程序只能串行发生。
很容易出现的问题就是计算机只能一个一个的处理,而且如果某一进程在进行读写操作,进程阻塞会带来CPU时间的浪费。
多进程/线程时代
后来操作系统具有最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来。
进程虚拟内存会占用4GB[32位操作系统],而线程也要大约4MB。 大量的进程/线程出现新的问题:高内存占用,以及频繁调度带来的cpu损耗。
线程分为“内核态”线程和“用户态”线程,一个“用户态”线程必须要绑定一个“内核态”线程。
但是CPU对“用户态”线程是无感知的。 这样,就把用户态的“线程”叫为了协程。
这个时候,就发展为了多个协程绑定到一个线程上的模式。
N:1
N个协程绑定1个线程。 优点是协程的切换非常快速,缺点是一旦某协程阻塞,造成线程阻塞,本进程的其它协程都会法执行。
![img](https://i-blog.csdnimg.cn/blog_migrate/684c7a2bb91d3ed3cd59fd3dd7503f00.jpeg)
1:1
1个协程1个线程。
协程的调度由CPU完成。
M:N
克服了以上两种模型的缺点,实现起来也是最复杂的。
![img](https://i-blog.csdnimg.cn/blog_migrate/c9a31facb6c7875f874be3735abc18bb.jpeg)
被废弃的goroutine调度器:GM
G表示Goroutine,M表示线程
M 想要执行、放回 G 都必须访问全局 G 队列,并且 M(线程) 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。
![img](https://i-blog.csdnimg.cn/blog_migrate/1e40714177f5bc929de0be43cdbff45a.jpeg)
缺点:
- 创建、销毁、调度 G 都需要每个 M 获取锁。
- M之间的频繁切换增加系统开销
GMP是什么?
在GM的基础上引进了P(处理器)。
通过调度器来把可运行的goroutine分配到工作线程上。
- 全局队列(Global Queue):存放等待运行的 G。
- P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
- P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有
GOMAXPROCS
(可配置) 个。 - M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
GMP设计策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
1)work stealing 机制
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
2)hand off 机制
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
抢占:
goroutine最多占用CPU10ms,防止其它goroutine被饿死。