Do not communicate by sharing memory; instead, share memory by communicating
GMP
G
- G即 Goroutine, 它包括堆栈、指令指针和其他对调度goroutines很重要的信息
- go不同版本Goroutine默认栈大小不同
- 线程是运行Goroutine的实体, 调度器的功能是把可以运行的Goroutine分配到工作线程上
M
- M即工作线程,所有M都是有线程栈的, 每个M代表了1个内核线程.OS调度器负责把内核线程分配到CPU的核上运行
- M必须和P关联才能运行G
- work stealing: 当M绑定的P没有可运行的G时,它可以从其他运行的M那里偷取G
- 线程想要运行任务需要获取P, 从P的本地队列获取G,
- P本地队列为空时, M也会尝试从全局队列获取一批G放到本地P的本地队列
- 或从其他P的本地队列偷一般放到自己P的本地队列
- M运行G, G执行之后, M会从P获取下一个G, 不断重复下去
P
- P即Processor是一个抽象的概念, 并不是真正的物理CPU, 它包含了运行Goroutine的资源
- 线程想运行G, 必须先获取P, P中包含了可运行的G队列
- P需要和M进行绑定, 构成一个执行单元
- P决定了同时可以并发任务的数量
通过runtime.GOMAXPROCS限制同时执行用户级任务的操作系统线程, Go1.5以后被设置可用的核数
- P有两种队列
- 本地队列
当前P的队列,本地队列是Lock-Free,没有数据竞争问题,无需加锁处理,可以提升处理速度。
存放的也是等待运行的G,存的数量有限- 全局队列
全局队列为了保证多个P之间任务的平衡。所有M共享P全局队列,
为保证数据竞争问题,需要加锁处理。相比本地队列,处理速度要低
GMP调度过程
正常情况下, 每个P(context)都会有local runqueue, 挂载若干的G, 其中只有一个G正在M上运行
每个P除了local runqueue, 所有的P还共享一个全局的global runqueue, 在某些情况下会将G挂载到这个global runqueue下,
-
当一个G系统阻塞了, 比如(调用了sleep或者I/O系统调用), G处于_Gsyscall状态,M也处于 block on syscall 状态,此时的M可被抢占调度,
由于线程不能同时执行代码和被阻塞在syscall上,所以我们需要传递上下文,以便它能够继续调度
go runtime却不会让对应的P(context)被阻塞(想一下,P的local runqueue下此时可能还挂了一串而goroutine呢) 而是去线程缓存池(M cached pool)中Get一个M出来如下图M1,把自己挂上去,从local runqueue上pop一个G出来继续执行;如果没有其它idle的M,但P的Local队列中仍然有G需要执行,则创建一个新的线程,并将P与之绑定,顺序执行P中下一个G.
上一步中,P带着自己的local runqueue 跑路了,扔下M被阻塞,但是当时触发阻塞的G却并不会跟着P一起跑路,(G是一个比较厚道的人);
M终于从blocked(e.g. I/O syscall)中返回了,此时想要继续执行这个G,但那是不可能的,因为G必须在P这个context下执行,所以这个M就去找空闲的P了,如果找到了,就把这个G挂上去开始执行,如果找不到,就把G加入到global runqueue(3中提到过,记得么?)中,而自己则很乖地去线程池报道(4中会用到);
为了避免global runqueue出现工地悲剧,每个P会时不时地去看下global runqueue是不是有G,避免这些G被饿死;(恩,大家都很nice呢);
当某个M-P下已经没有G的时候,P就会难掩助人为乐的精神,随机地跑到其它P下偷一半的G过来自己执行( 如下图)
-
当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。
Stealing work
参考文献:
有疑问加站长微信联系(非本文作者)