调度的基础,模型关系的映射
GPM模型:
- G,Goroutinue
- 被调度器管理的轻量级线程,goroutine使用go关键字创建
- 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。默认的大小是2KB,根据需要逐步上涨。
- G绑定到P上执行
- P,Processor
- 逻辑执行单元
- 存储了M执行的上下文,包括各种G对象队列、链表、cache和状态
- G存在于P中的特定链表上,同一时刻,P只能在一个M上,因此不需要锁
- M,Machine
- 操作系统的实际的线程
- OS的执行的单位,Linux下的大小是8MB
- M绑定执行的P,但是不保存P的状态,因此P可以跨M执行
整体的调度关系
每个M都有一个P,绿色的G表示当前M上运行的G,灰色的表示local G queue
从上图理解出,P是G实际的运行绑定的单位,因此P的数量决定了可以并发的G数量;又因为P最终是绑定到M上的,因此M的数量决定了最终的并行的数量。在Golang中,P的数量由runtime.GOMAXPROCS
来控制,M取决于硬件,默认情况下,等于OS的线程数量。
P的队列空了之后,不会一直闲置,而是会从其它P中或者全局G queue中,如下图:
关于Global G Queue和每个P的Local G Queue产生方式:
go
关键字生成一个G,之后G会尝试放入当前的P的Local G Queue中,如果失败了,就放入Global G Queue。
如果G发生阻塞,则会尝试寻找新的G来运行,阻塞的G返回后重新加入G Queue中。
P在轮询查找G的时候,每隔61次从Global G Queue中查找,保证Global也可以执行。当一个G执行超过10ms时,schedule会有对应的抢占机制。
一些底层知识
线程切换与协程切换的区别。LTS(Local Thread Storage)存储了线程执行需要的堆栈信息,寄存器的数据等,之后线程会load 程序并执行。对于执行中的进程,在对应的地址其实位置,同样会启动线程,此时OS会分配对应的内存空间,并启动执行。Linux系统中,1个线程是启动的大小8MB,而且启动和上下文切换会消耗对应的时间。
协程的特点,协程不是OS级别的,因此协程的功能是程序内部调度的。OS感觉不到协程的存在,因为OS根本就没有协程的概念!!!
Golang为协程的代码段,在堆上分配初始化的2KB的空间,之后进入之前提到的调度流程。一般来说,Golang使用了线程复用的方式,即启动线程的时候,在上面有协程运行,协程停止或者阻塞的时候,不会主动停止线程,而是更改线程的FS寄存器的值到对应的协程代码段上,然后此时线程执行的位置就是新的协程的代码位置了,此时协程切换的代价是改变线程执行的位置,然后执行新的协程,因此代价很小。
这边可能要后期更正,FS寄存器那边的概念不是特别清楚
具体调度方案
给出整体的调度状态切换图:
sysmon
:抢占式调度系统,对于执行时间超过10ms的G,会更正为可抢占的,其他协程可以抢占该G的执行,防止被一个G一直占用。
以下几种情况会导致Goroutinue阻塞,进而让出P,使得P与其它G绑定,高效利用CPU:
- syscall:系统调用,比如读写长文本等
- Network IO:网络传输数据等
- channel获取不到数据
- sync包的调用
参考资料:
- https://zboya.github.io/post/go_scheduler/
- https://blog.csdn.net/u010853261/article/details/84790392#Section1_Scheduler_9
- http://www.sizeofvoid.net/goroutine-under-the-hood/