调度器的出现
单进程操作系统,一次只能运行一个任务,不存在调度器。
多进程操作系统,允许并发执行多个任务,有调度器,多个任务根据时间片来分别执行任务。(时间片执行完会强制执行下一个时间片中的任务,进程主动让出或挂起也会让出时间片)
缺点:时间片执行带来的是进程间的切换,涉及到系统调用和中断(上下文环境复制,要和寄存器打交道)。
即从用户态陷入系统态,再切入到用户态。即调度很消耗资源,进程一旦多起来,可能切换时间要大于执行时间。
因为进程和线程的创建成本高(线程
4M),切换成本高,体现在CPU占用高,内存占用高,所以出现了协程。线程又分用户线程,内核线程,分指不同环境下的线程。
CPU只能看到内核线程。内核线程是真线程,能分配到CPU资源。起初为一个用户线程绑定一个内核线程。用户线程又叫协程。即协程绑定在内核线程之上。
N:1问题:会引起阻塞。1:1无效果。M:N实现复杂,能用多核。

协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。
Golang 协程调度器历史版本
2012 之前被废弃版本
初版
有一个全局协程队列,G队列。多个线程M,访问或者放回G,都需要通过G队列,并且加互斥锁。这个版本没有调度器,创建销毁调度全由内核线程完成。**M**需要去取回**G**执行**G**中的任务。
缺点:
1.创建销毁调度都需要_M_对其整个队列上锁,容易形成锁竞争。
2._M_转移会造成延迟和额外的系统负载。(_M1_拿回_G1_执行任务的过程中,这个_G1_又创建了一个_G2_,按道理来说新创建的_G2_,应当在_M1_中执行,但是这时新创建的_G2_是不一定会在M1中执行的,即造成了_M_转移)
第二版

- 全局队列(Global Queue):存放等待运行的
G。 - P的本地队列:同全局队列类似,存放的也是等待运行的
G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。 - P列表:所有的
P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。 - M:线程想运行任务就得获取
P,从P的本地队列获取G,P的本地队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
其中M是动态的,空闲可能会被回收,阻塞P会再创建一个M。也可以手动设置。
协程的设计策略:
- 避免频繁创建和销毁线程,可以对线程
M进行复用。当M从P中取不到G了也不会闲着,从而避免频繁创建和销毁。有两种机制不闲着:
work stealing机制。空闲时(即当前绑定的P中没有G了),就会去尝试从其他P中偷取回G放入自己的P中。偷不到就尝试从全局队列中偷取,再偷不到就空闲了。hand off机制。当M因为G进行系统调用阻塞时,M释放绑定的P,阻塞G和M直接绑定。然后把P转移给其他空闲的M执行。阻塞G绑定的M执行完毕后,M进入睡眠或者销毁(避免频繁创建)。
以上机制应该是由协程调度器来执行。M只负责执行。其M的创建销毁等也是由协程调度器来执行。可以将M理解为是死的,无情的执行机器,是由调度器喂G给M吃。因为M在内核态,内核不会做这些事情。
- 实现并行。看上图,多个
M在P队列中拿G执行。 - 抢占。
coroutine是需要等待让出。goroutine是抢占式,最多占用CPU 10ms。这是和coroutine不同的地方之一。问题。既然一个goroutine最多只占用10ms,那要是没执行完怎么办?放在后面 链接。
4.全局队列。当M执行work stealing从其他G中偷取G又没偷到时,则去全局队列中偷取回P中再执行。
go func()调度流程
超过10ms怎么办?
https://www.lmlphp.com/user/150984/article/item/2704287/
那就会有个问题,如果一个系统调用或者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. 当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。 ~(≧▽≦)/~
GO调度器的启动周期
M0:第一个线程,负责执行初始化操作和启动第一个G。启动完成后该M0就和其他M一样了。
G0:每个M都有一个G0,仅用于调度M要使用的G。每次调度先会切换到G0,执行G0的调度程序,G0就会拉取下一个要执行的G到M中执行。


