go runtime M:N模型可以根据具体的操作类型(操作系统阻塞或非阻塞操作)调整goroutine和OS Thread的映射情况,更加灵活
两个M,即两个OS Thread线程,分别对应一个P,每一个P有负责调度多个G。组成的goroutine运行时的基本结构
G最重要的三个状态
-
Grunnable
-
Grunning
-
Gwaiting
状态迁移 Grunnable -> Grunning -> Gwaiting -> Grunnable
goroutine在状态发生转变时,会对栈的上下文进行保存和恢复
G中的Gobuf的定义
struct Gobuf{ uintptr sp; // 栈指针 uintptr pc; // 程序计数器PC G* g; // 关联的G};
保存栈上下文时,最重要的就是保存这个Gobuf结构中的内容
具体是通过 void gosave(Gobuf*)
以及 void gogo(Gobuf*)
实现栈上下文的保存和恢复
底层实现为汇编,因此goroutine的context swtich非常快
goroutine scheduler在几个主要场景下的调度策略
goroutine将scheduler的执行交给具体的M,即OS Thread
每一个M就执行一个函数,即 void schedule(void)
这个函数具体做得事情就是从各个运行队列中选择合适的goroutine然后执行goroutine中对应的 func
// 调度的一个回合:找到可以运行的G,执行// 从不返回static void schedule(void){ G *gp; uint32 tick;top: gp = nil;// 时不时检查全局的可运行队列,确保公平性// 否则两个goroutine不断地互相重生,完全占用本地的可运行队列 tick = m->p->schedtick;// 优化技巧,其实就是tick%61 == 0if(tick - (((uint64)tick\*0x4325c53fu)>>36)\*61==0&& runtime·sched.runqsize >0) { runtime·lock(&runtime·sched); gp = globrunqget(m->p, 1);// 从全局可运行队列获得可用的G runtime·unlock(&runtime·sched);if(gp) resetspinning(); }if(gp == nil) { gp = runqget(m->p); // 如果全局队列里没找到,就在P的本地可运行队列里找if(gp && m->spinning) runtime·throw("schedule: spinning with local work"); }if(gp == nil) { gp = findrunnable(); // 阻塞住,直到找到可用的G resetspinning(); }// 是否启用指定某M来执行该Gif(gp->lockedm) {// 把P给指定的m,然后阻塞等新的P startlockedm(gp); goto top; } execute(gp); // 执行G}
几个问题:
- 当M发现分配给自己的goroutine链表已经执行完毕时怎么办?
- 当goroutine陷入系统调用阻塞后,M是否也一起阻塞?
- 当某个gorouine长时间占用CPU怎么办?
一
-
从其他的P中偷取goroutine然后执行,略就是工作密取的机制
-
当其他的P也没有可执行的goroutine时,从全局等待队列中寻找runnable的goroutine进行执行
-
如果还找不到,则M让出CPU调度
二
例如阻塞IO读取本地文件,此时调用会systemcall会陷入内核,不可避免地会使得调用线程阻塞
goroutine将所有可能阻塞的系统调用均封装为gorouine友好的接口
具体,在每次进行系统调用之前,从一个线程池从获取一个OS Thread并执行该系统调用,而本来运行的gorouine则将自己的状态改为Gwaiting,并将控制权交给scheduler继续调度,系统调用的返回通过channel进行同步即可
因此其实goroutine也没有办法做到完全的协程化,因为系统调用总会阻塞线程
三
go支持简单的抢占式调度,sysmon线程检测goruntime的各种状态,长时间占用CPU的goroutine,如果发现了就将其抢占过来
goroutine 中最重要的一个设计就在于它将所有的语言层次上的api都限制在了goroutine这一层,进而屏蔽了执行代码与具体线程交互的机会
实际上就可以忽略线程的存在,把goroutine当成是一个非常廉价能够大量创建的Thread
JVM系语言(如scala,clojure),本质上都无法完全实现goroutine(clojure虽然有async,但是依然无法和JDK中的阻塞api结合良好)
假设在Java中基于Thread之上实现一个scheduler,一个轻量级的协程以及协程相关的原语(如resume, pause等),我们也只能基于我们自己封装的api来协助协程调度
如果在创建的协程中直接使用Java阻塞api,结果就是使用来调度协程的OS Thread陷入阻塞,无法继续运行scheduler进行调度