[转]golang的goroutine调度机制

golang的goroutine调度机制linux

 

 
 

一直对goroutine的调度机制很好奇,最近在看雨痕的golang源码分析,(基于go1.4)

感受豁然开朗,受益不浅;api

去繁就简,再加上本身的一些理解,整理了一下数组

~~函数

调度器工具

主要基于三个基本对象上,G,M,P(定义在源码的src/runtime/runtime.h文件中)源码分析

1.     G表明一个goroutine对象,每次go调用的时候,都会建立一个G对象post

2.     M表明一个线程,每次建立一个M的时候,都会有一个底层线程建立;全部的G任务,最终仍是在M上执行优化

3.     P表明一个处理器,每个运行的M都必须绑定一个P,就像线程必须在么一个CPU核上执行同样

P的个数就是GOMAXPROCS(最大256),启动时固定的,通常不修改; M的个数和P的个数不必定同样多(会有休眠的M或者不须要太多的M)(最大10000);每个P保存着本地G任务队列,也有一个全局G任务队列;

以下图所示

全局G任务队列会和各个本地G任务队列按照必定的策略互相交换(满了,则把本地队列的一半送给全局队列)

P是用一个全局数组(255)来保存的,而且维护着一个全局的P空闲链表

每次go调用的时候,都会:

1.     建立一个G对象,加入到本地队列或者全局队列

2.     若是还有空闲的P,则建立一个M

3.     M会启动一个底层线程,循环执行能找到的G任务

4.     G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找(一次性转移(全局G个数/P个数)个,再去其它P中找(一次性转移一半),

5.     以上的G任务执行是按照队列顺序(也就是go调用的顺序)执行的。(这个地方是否是以为很奇怪??)

对于上面的第2-3步,建立一个M,其过程:

1.     先找到一个空闲的P,若是没有则直接返回,(哈哈,这个地方就保证了进程不会占用超过本身设定的cpu个数)

2.     调用系统api建立线程,不一样的操做系统,调用不同,其实就是和c语言建立过程是一致的,(windows用的是CreateThread,linux用的是clone系统调用),(*^__^*)嘻嘻……

3.     而后建立的这个线程里面才是真正作事的,循环执行G任务

那就会有个问题,若是一个系统调用或者G任务执行太长,他就会一直占用这个线程,因为本地队列的G任务是顺序执行的,其它G任务就会阻塞了,怎样停止长任务的呢?(这个地方我找了很久~o(╯□╰)o)

这样滴,启动的时候,会专门建立一个线程sysmon,用来监控和管理,在内部是一个循环:

1.     记录全部P的G任务计数schedtick,(schedtick会在每执行一个G任务后递增)

2.     若是检查到 schedtick一直没有递增,说明这个P一直在执行同一个G任务,若是超过必定的时间(10ms),就在这个G任务的栈信息里面加一个标记

3.     而后这个G任务在执行的时候,若是遇到非内联函数调用,就会检查一次这个标记,而后中断本身,把本身加到队列末尾,执行下一个G

4.     O(∩_∩)O哈哈~,若是没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用的话,那就惨了,会一直执行这个G任务,直到它本身结束;若是是个死循环,而且GOMAXPROCS=1的话,恭喜你,夯住了!亲测,的确如此

对于一个G任务,中断后的恢复过程:

1.     中断的时候将寄存器里的栈信息,保存到本身的G对象里面

2.     当再次轮到本身执行时,将本身保存的栈信息复制到寄存器里面,这样就接着上次以后运行了。 ~\(≧▽≦)/~

 

可是还有一个问题,就是系统启动的过程,雨痕没有说的太明白,我一直有不少问题都狠疑惑(第一个M怎么来的?,G怎么找到对应的P?等等),这个让我蛋疼了很久~

不过我本身意淫了一下,补充在下面,欢迎你们指正

1.     系统启动的时候,首先跑的是主线程,那第一个M应该就是主线程吧(按照C语言的理解,嘿嘿),这里叫M1,能够看前面的图

2.     而后这个主线程会绑定第一个P1

3.     我们写的main函数,实际上是做为一个goroutine来执行的(雨痕说的)

4.     也就是第一个P1就有了一个G1任务,而后第一个M1就执行这个G1任务(也就是main函数),建立这个G1的时候不用建立M了,由于已经有了M1

5.     这个main函数里面全部的goroutine,都绑定到当前的M1所对应的P1上,O(∩_∩)O哈哈~

6.     而后建立main里的goroutine的时候(好比G2),就会建立新的M2,新的M2里的初始P2的本地任务队列是空的,会从P1里面取一些过来,哈哈

7.     这样两个M1,M2各自执行本身的G任务,再依次往复,这下就圆满了~~~

 

综上:

因此goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换做下一个

这个和目前主流系统的的cpu调度相似(按照时间分片)

windows:20ms

linux:5ms-800ms


到这里都差很少了,这些在雨痕的笔记里面都有更详细的描述,不过不少地方比较凌乱,比较复杂,这里筛检了不少,方便读者理解

 

注意:

1.     在Golang中编译器也会尝试进行内联,将小函数直接复制并编译,为了内联,尽可能消除编译器没法侦测的dead code,利用gobuild -gcflags=-m编译命令能够查看程序内联状态,不得不说golang的编译工具链仍是很强大的,十分有利于程序的优化。

 

若是有任何疑问,欢迎提出,

随时更新

 

 

(这篇文章是去年整理的,记录公司内部wiki上~)

相关文章
相关标签/搜索