并发调度MPG(解释1)
Processor(简称 P),其作用类似 CPU 核,用来控制可同时并发执行的任务数量。 每个工作线程都必须绑定一个有效 P 才被允许执行任务,否则只能休眠,直到有空闲 P 时 被唤醒。P 还为线程提供执行资源,比如对象分配内存、本地任务队列等。线程独享所绑 定的 P 资源,可在无锁状态下执行高效操作。
进程内的一切都在以 goroutine(简称 G)方式运行,包括运行时相关服务,以及 main.main入口函数。需要指出,G 并非执行体,它仅仅保存并发任务状态,为任务执行提供所需栈内存空间。G 任务创建后被放置在 P 本地队列或全局队列,等待工作线程调度执行.
实际执行体是系统线程(简称 M),它和 P 绑定,以调度循环方式不停执行 G 并发任务。
M 通过修改寄存器,将执行栈指向 G 自带栈内存,并在此空间内分配堆栈帧,执行任务
函数。当需要中途切换时,只要将相关寄存器值保存回 G 空间即可维持状态,任何 M 都
可据此恢复执行。线程仅负责执行,不再持有状态,这是并发任务跨线程调度,实现多路
复用的根本所在。
尽管 P/M 构成执行组合体,但两者数量并非一一对应。通常情况下,P 数量相对恒定,默
认与 CPU 核数量相同,但也可能更多或更少,而M 则是调度器按需创建。举例来说,当
M 因陷陷入系统调用长时间阻塞时,P 就会被监控线程抢回,去新建(或唤醒)一个 M 执
行其他任务,如此 M 的数量就会增长。
因为 G 初始栈仅有 2KB,且创建操作只是在用户空间简单的对象分配,远比进入内核态
分配线程要简单得多。调度器让多个 M 进如调度循环,不停获取并执行任务,所以我们
才能创建成千上万个并发任务。
并发调度MPG(解释2)
Golang的最小调度单元为协程,操作系统最小的调度单元是线程,Golang调度器将众多的goroutine放在有限的线程上进行高效而公平的调度。协程比线程更加轻量级,创建几万几十万的goroutine而不用担心内存耗尽等问题。
M:os线程(即操作系统内核提供的线程)
P:M与P的中介,实现m:n 调度模型的关键,M必须拿到P才能对G进行调度,P其实限定了golang调度其的最大并发度。
G:goroutine,其包含了调度一个协程所需要的堆栈以及instruction pointer(IP指令指针),以及其他一些重要的调度信息。
golang对这层调度其实做了一定的优化,不是在一开始进行系统调用之前就新建一个新的M,而是使用一个全局监控的monitor(sysmon),定期检查进入系统调用的M,只有进入时间过长才会新建一个M。另外golang 底层基本对所有的IO都异步化了,比如网络IO,golang底层在调用read返回EAGAIN错误的时候会将当前goroutine挂起,然后使用epoll监听这个网络fd上的可读事件,在当有数据可读的时候唤醒对应的goroutine继续进行调度。其中epoll事件管理线程即golang虚拟机的sysmon线程。
当前运行的goroutine在达到调度点(系统调用、网络IO、等待channel等)的时候,P会挂起当前运行的goroutine,从runqueue中pop一个goroutine,重新设置当前M的执行上下文继续执行(即设置为pop出来的goroutine对应的运行堆栈以及IP(instruction Point))。
golang语言能够控制线程M在用户态执行情况下的调度行为,但是却不能控制线程在陷入内核之后的行为。即在我们调用system call陷入内核没有返回之前,go其实已经控制不了。那怎么办呢?如果当前陷入system call的线程在持有P的情况下Block很长时间,会导致其他可运行的goroutine由于没有可用的P而不能得到调度。
为了保证调度的并发型,即保证拿到P的M能够持续进行调度而不是block,golang 调度器也没有很好的办法,只能在进入系统调用之前从线程池拿一个线程或者新建一个线程的方式,将当前P交给新的线程M1从而使得runqueue中的goroutine可以继续进行调度。当前M0则block在system call中等待G0返回。 在G0返回之后需要继续运行,而继续运行的条件是必须拥有一个P,如果当前没有可用的P,则将G0放到全局的runqueue中等待调度,M0退出或者放入线程池睡觉去。而如果有空闲的P,M0在拿到P之后继续进行调度。(P的数量很好的控制了并发度)
P在当前local runqueue中的G全部调度完之后从global runqueue中获取G进行调度,同样系统也会定期检查golobal runqueue中的G,以确保被放入global runqueue中的goroutine不会被饿死。
并发调度MPG(解释3)
G 受管理的轻量线程
goroutine的新建, 休眠, 恢复, 停止都受到go运行时的管理.
goroutine执行异步操作时会进入休眠状态, 待操作完成后再恢复, 无需占用系统线程,
goroutine新建或恢复时会添加到运行队列, 等待M取出并运行.
M golang中等同于系统线程.
go代码, 即goroutine, M运行go代码需要一个P
原生代码, 例如阻塞的syscall, M运行原生代码不需要P
M会从运行队列中取出G, 然后运行G, 如果G运行完毕或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始.
有时候G需要调用一些无法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其他M会取得这个P并继续运行队列中的G.
go需要保证有足够的M可以运行G, 不让CPU闲着, 也需要保证M的数量不能过多.
P 代表M运行G所需要的资源.
如果P的数量等于1, 代表当前最多只能有一个线程(M)执行go代码,
如果P的数量等于2, 代表当前最多只能有两个线程(M)执行go代码.
执行原生代码的线程数量不受P控制.
因为同一时间只有一个线程(M)可以拥有P, P中的数据都是锁自由(lock free)的, 读写这些数据的效率会非常的高.
Lock-Free 是指能够确保执行它的所有线程中至少有一个能够继续往下执行。由于每个线程不是 starvation-free 的,即有些线程可能会被任意地延迟,然而在每一步都至少有一个线程能够往下执行,因此系统作为一个整体是在持续执行的,可以认为是 system-wide 的。所有 Wait-free 的算法都是 Lock-Free 的。
G的状态
空闲中(_Gidle): 表示G刚刚新建, 仍未初始化
待运行(_Grunnable): 表示G在运行队列中, 等待M取出并运行
运行中(_Grunning): 表示M正在运行这个G, 这时候M会拥有一个P
系统调用中(_Gsyscall): 表示M正在运行这个G发起的系统调用, 这时候M并不拥有P
等待中(_Gwaiting): 表示G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
已中止(_Gdead): 表示G未被使用, 可能已执行完毕(并在freelist中等待下次复用)
栈复制中(_Gcopystack): 表示G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)
M的状态
M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:
自旋中(spinning): M正在从运行队列获取G, 这时候M会拥有一个P
执行go代码中: M正在执行go代码, 这时候M会拥有一个P
执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P
休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P
自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量.
P的状态
空闲中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
运行中(_Prunning): 当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
系统调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
GC停止中(_Pgcstop): 当gc停止了整个世界(STW)时, P会变为此状态
已中止(_Pdead): 当P的数量在运行时改变, 且数量减少时多余的P会变为此状态
通用内核名词,函数解释
systemstack()
systemstack会切换当前的g到g0, 并且使用g0的栈空间, 然后调用传入的函数, 再切换回原来的g和原来的栈空间,切换到g0后会假装返回地址是mstart, 这样traceback的时候可以在mstart停止.
这里传给systemstack的是一个闭包, 调用时会把闭包的地址放到寄存器rdx, 具体可以参考上面对闭包的分析.
TLS(Thread Local Storage)
线程局部存储。
getg()
goget()用来获取当前线程正在执行的协程g。该协程g被存储在TLS中。
mcall()
mcall在golang需要进行协程切换时被调用,用来保存被切换出去协程的信息,并在当前线程的g0协程堆栈上执行新的函数。一般情况下,会在新函数中执行一次schedule()来挑选新的协程来运行。接下来我们就看看mcall的实现。
程序初始化
go程序的入口点是runtime.rt0_go, 流程是:
调用runtime.schedinit执行共同的初始化
这里的处理比较多, 会初始化栈空间分配器, GC, 按cpu核心数量或GOMAXPROCS的值生成P等
生成P的处理在procresize中
调用runtime.newproc创建一个新的goroutine, 指向的是runtime.main
runtime.newproc这个函数在创建普通的goroutine时也会使用, 在下面的"go的实现"中会详细讲解
调用runtime·mstart启动m0
启动后m0会不断从运行队列获取G并运行, runtime.mstart调用后不会返回
runtime.mstart这个函数是m的入口点(不仅仅是m0), 在下面的"调度器的实现"中会详细讲解
G里面比较重要的成员如下
stack: 当前g使用的栈空间, 有lo和hi两个成员
stackguard0: 检查栈空间是否足够的值, 低于这个值会扩张栈, 0是go代码使用的
stackguard1: 检查栈空间是否足够的值, 低于这个值会扩张栈, 1是原生代码使用的
m: 当前g对应的m
sched: g的调度数据, 当g中断时会保存当前的pc和rsp等值到这里, 恢复运行时会使用这里的值
atomicstatus: g的当前状态
schedlink: 下一个g, 当g在链表结构中会使用
preempt: g是否被抢占中
lockedm: g是否要求要回到这个M执行, 有的时候g中断了恢复会要求使用原来的M执行
M里面比较重要的成员如下
g0: 用于调度的特殊g, 调度和执行系统调用时会切换到这个g
curg: 当前运行的g
p: 当前拥有的P
nextp: 唤醒M时, M会拥有这个P
park: M休眠时使用的信号量, 唤醒M时会通过它唤醒
schedlink: 下一个m, 当m在链表结构中会使用
mcache: 分配内存时使用的本地分配器, 和p.mcache一样(拥有P时会复制过来)
lockedg: lockedm的对应值
P里面比较重要的成员如下
status: p的当前状态
link: 下一个p, 当p在链表结构中会使用
m: 拥有这个P的M
mcache: 分配内存时使用的本地分配器
runqhead: 本地运行队列的出队序号
runqtail: 本地运行队列的入队序号
runq: 本地运行队列的数组, 可以保存256个G
gfree: G的自由列表, 保存变为_Gdead后可以复用的G实例
gcBgMarkWorker: 后台GC的worker函数, 如果它存在M会优先执行它
gcw: GC的本地工作队列, 详细将在下一篇(GC篇)分析
参考:
https://studygolang.com/articles/3491
https://blog.csdn.net/m0_37579159/article/details/79345831
https://studygolang.com/articles/11863?fr=sidebar
https://studygolang.com/articles/11862
https://zhuanlan.zhihu.com/p/27328476?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com
https://zhuanlan.zhihu.com/p/27400277
https://www.cnblogs.com/zkweb/category/1108329.html
https://zhuanlan.zhihu.com/golang-internal
https://blog.csdn.net/heiyeshuwu/article/details/51178268
说说Golang的runtime
https://zhuanlan.zhihu.com/p/27328476?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com
golang密集场景下协程调度饥饿问题
http://xiaorui.cc/2018/06/04/golang%E5%AF%86%E9%9B%86%E5%9C%BA%E6%99%AF%E4%B8%8B%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E9%A5%A5%E9%A5%BF%E9%97%AE%E9%A2%98/
Golang 之协程详解(对比其他网络模型优缺点)
https://www.cnblogs.com/liang1101/p/7285955.html