调度器 GMP:线程是有pcb的内核级线程,协程是用户级线程。goroutinue占内存更小(几kb)调度更灵活(runtime调度)
- 三个主要组件:goroutine(G)、processor(P )、thread(M),P是新的GMP引入的组件,包含运行goroutine的资源,M是运行goroutine的实体。Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。
- 其他组件:
- 全局队列:存放等待运行的G
- P的本地队列:P的存放等待的G,新建G时会优先放在自己的本地队列里 。(本地队列若满将前一半的G放在全局队列中)
- P列表:所有的P启动时创建保存在数组里
- M运行任务要获得P,从P本地队列获取P 队列为空时,M 会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取G,不断重复。创建G时,运行的G会尝试唤醒其他空闲的P和M组合执行,若绑定后P本地队列没有G则会自旋线程(没有G但为运行态线程不断寻找G),自旋线程数量有限为了线程复用。
- go func()调度流程
- 通过 go func () 来创建一个 goroutine;
- 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;
- G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会向其他的 MP 组合偷取一个可执行的 G 来执行;
- 一个 M 调度 G 执行的过程是一个循环机制;
- 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;
- 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。
- 调度器的生命周期:创建第一个M0->创建第一个G0->调度初始化->创建main()的g->启动M0->M绑定P->M通过P获取到G(若获取不到G则令M休眠等待被唤醒然后再绑定)->M设置G环境->M执行G->G退出->M通过P获取G->…
- M0是启动程序后编号为0的主线程,不在heap,M0负责初始化启动第一个M。G0每个M启动时创建的第一个g,G0仅用于负责调度