Golang的协程调度

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

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/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章