Go的GMP调度器
前言:
最后有讲系统级别多线程、多进程的问题.
最大的问题其实就是资源浪费,切换成本高
所以导致很多语言对线程的控制要求比较高,否则容易出现内存溢出,或者频繁的让cpu滴使用
率达过高
Go的GMP就很好的解决了这部分的问题。这也是Go逐渐成为热门语言的原因之一。
线程补充:
线程是系统级别进行控制(系统态)。
线程会由机器的多核数量来达到是否存在并行的情况。
协程 co-routine
是有用户态进行管理操作
CPU
cpu只看系统态,只会看向线程,从而在线程和协程中,还有着调度器进行分配管理。
通俗一点的说法就是:通过调度器把不同的协程放入线程执行.(不理解的这么想也是可以的)
那么如何匹对呢?
线程和协程的对应关系
假设
情况一:一个线程通过调度器对应多个的协程,那么在普通情况下确实会加快效率,节省cpu调度,但是
如果协程发生阻塞,那该线程也会陷入等待,后面的协程也将会陷入等待。
情况二: 一对一,一个线程对应一个协程,那这种情况就和普通的多线程系统无异,差不多就是全交予
系统进行处理。
情况三:多对多,多个线程对应多个协程,按照这样的思路,确实可以进行有效的切换,重点在于调
度器是如何分配线程和协程的了。
GMP
G : goroutine 协程
M : thread 线程
M列表:当前操作系统分配到Go程序的内核线程数
(限制最大量是10000 可忽略 根本不会开着这么多 当然也可通过代码进行调整)
当M发生阻塞时,则会创建新的M。
当M空闲时,那么就会回收或者睡眠。
所以M的数量是动态的。
P:处理器
和P有关的特征:
p每个处理器的个数会对应着有本地队列,来存放G
本地队列大小 不超过 256G
全局队列:存放等待运行的G
一般都是优先会把写成存放在P的本地队列中,满了之后就会存放在全局队列。
程序启动时创建
最多有GOMAXPROCS个(可配置)(简称核数)
使用用户态来控制协程为何就会效率高一些.
1.复用线程(避免了多数的无效切换)
2.机制保证了利用率
偷拿:假设一个P1队列里包含了多个G协程,那么如果P2是空闲状态,则会把P1队列里的G
偷偷拿到自己的本地队列的执行。
分离:如果P1队列里存在G1、G2、G3,但是在执行G1时发生了阻塞,让其他G陷入了等待,
这个时候就会重新创建或者唤醒一个M,把整个P里的队列分离出去,分到新的M上进行
执行,使CPU绑定新的M,而G1的M则不会被CPU绑定,停止运行,最后再来决定是否
还需要运行,如果不需要,那么之前的M,则可以直接销毁或者睡眠,如果还需要工作运
行,则会把G1重新安排到其他队列当中等待运行。
3.抢占策略
以前:co-routine协程当占用cpu进行执行时,如果没有发生阻塞,则会一直执行下去,直到
执行完毕主动释放才能轮到第二个协程执行。
go-toutine协程:每个协程执行的时间为10ms,随后再有其他协程执行,这样就是一个平均
执行的概念,没有优先级概念。
创建协程存入队列的顺序
优先级:
1.本地队列:也就是执行代码逻辑的该线程M,那么本协程就会存入对应的P本地队列当中
2.全局队列:当本地队列存储已满时,则会进入到全局队列,
这边会有误区:
误区1:是不是只有当一个p的本地队列满了之后才会存入全局队列这一种
情况,当然不是,之前讲的分离,在阻塞时,当创建完M执行一系列过程后,如果该
协程阻塞完还是会需要执行,则会直接分配到全局队列当中,等待被执行。
误区2:当本地队列存储已满时,比如已经存在了四个G1、G2、G3,G4,最后加入的是G5,
是不是只有G5才会被加入到全局队列中去,当然不是,这边会把G1、G2顺序打乱,也加入
到全局队列当中,规则就是把当前本地队列中的G对半分,前面一半的G打乱顺序存放到
全局队列当中,并且附加上G5.
3.偷拿,被cpu绑定的M空闲时,会优先获取全局队列中的G,如果全局队列中也没有G,
则会在其他P本地队列当中获取G.
--end结束-- 所有的具体调度都会通过一个G0的协程来,G0只做协程调度,也就是程序最开始创建的协程. G0存放着上下文切换的环境,等。 当M执行完一个协程时,则会调用G0来分配下一个协程的调用,而调用下一个协程的逻辑就是上面讲的优先级顺序了。