Golang线程调度

  1. 调度器的三个抽象概念:G、M、P

G:代表一个goroutine,每个goroutine都有自己独立的栈存放当前的运行内存及状态,可以把一个G当作一个任务。

M:代表内核线程,它本身就与一个内核线程进行绑定,goroutine运行在M上。

P:代表一个处理器,可以认为一个”有运行任务“的P占了一个CPU的线程的资源,且只要处于调度的时候就有P。

注:内核线程指运行在内核上的线程,可以有上万个,但是CPU线程没有那么多,也就是Top命令中看到的CPU0、CPU1…的数量。

三者关系大致如图所示

https://mmbiz.qpic.cn/mmbiz/jE5bOw22iaBsvRe8WJeliaBArqOEDx5jpxCNTW8niadMSGRUcs5EljM7Uk44I9M4wI4f77Q3rkZsNTHvPYYpickkTA/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

没有运行任务时,M依然与一个内核线程绑定,但不占用CPU线程,也不占用P。

### 循环调度

https://mmbiz.qpic.cn/mmbiz/jE5bOw22iaBsvRe8WJeliaBArqOEDx5jpxej9ic4peKBWD8YzMrgicxS4wAEohuOiczbfhmhltnK5MYnBjkmCPdCPag/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

图1代表M启动的过程,把M跟一个P绑定再一起。在程序初始化的过程中说到在进程启动的最后一步启动了第一个M(即M0),这个M从全局的空闲P列表里拿到一个P,然后与其绑定。而P里面有2个管理G的链表(runq 存储等待运行的G列表,gfree 存储空闲的G列表),M启动后等待可执行的G。

图2代表创建G的过程。创建完一个G先扔到当前P的 runq 待运行队列里。

在图3的执行过程里,M从绑定的P的 runq 列表里获取一个G来执行。当执行完成后,图4的流程里把G仍到 gfree 队列里。注意此时G并没有销毁(只重置了G的栈以及状态),当再次创建G的时候优先从 gfree 列表里获取,这样就起到了复用G的作用,避免反复与系统交互创建内存。

M即启动后处于一个自循环状态,执行完一个G之后继续执行下一个G,反复上面的图2~图4过程。当第一个M正在繁忙而又有新的G需要执行时,会再开启一个M来执行。

### 多个线程下如何调度

如何保证P中的G能均衡运行,不会出现一方过载一方空闲?

https://mmbiz.qpic.cn/mmbiz/jE5bOw22iaBsvRe8WJeliaBArqOEDx5jpxdaZlELjIYicotLvHEBZiasucicHOvI0R3l1qf4Jv6vDh9nmdcK41aVKqw/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

两种途径:

  1. 借助全局队列sched.runq作为中介,本地P里的G太多的话就放到全局,太少就从全局中取。
  2. 全局列表里没有的话可以从其他队列中偷取。