调度的基础,模型关系的映射

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

runtime.GOMAXPROCS
go

如果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

以下几种情况会导致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/