开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:MPG 模式的介绍】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/626/detail/9749
MPG 模式的介绍
内容介绍
一,MGP 模式基本介绍
二,MPG 模式运行的状态1
三,MPG 模式运行的状态2
一,MGP 模式基本介绍
Go 的调度器内部有三个重要的结构: M,P,S
M:代表真正的内核 0S 线程,和 POSIX 里的 thread 类似,真正起作用的
G:代表一个 goroutine,有自己的栈,instruction pointer 和其他信息(正在等待的 channel 等等),用于调度。
P:代表调度的上下文,可以把它看做一个局部的调度器,使 go 代码在一一个线程上跑,是实现从 N:1到 N:M 映射的关键。
第一个结论,看一下主线程,它是一个物理线程,主线程是直接作用在一个 cpu 上面的是重量级的非常的耗费 cpu 资源组建,一个物理级的操作系统来控制的携程,是从主线程开启的,是轻量级的线程是相对来说资源消耗相对较小,形成机制重要的一个特点,可以轻松的开启三个携程和其他编程,语言的并发机制一般是基于线程那开启过的线程资源耗费大。这个快速入门的一个小节先把它反射到里面后接着快速度将它关掉。
快速入门重点就是做了三个的缩影。接着,来检验一下 gro ton,的一个调度模型是什么机制。这个 MPG 一般就是面试。第一个代表是操作系统的一个主线测。M 是一种相当于是一个物理极限,所以它比较费资源。P 可以认为是一个在整个执行过程中的一个上下文环境,上下文环境是在很多编程语言里面都会有提到的东西,上下文环境,可以简单的说运行的时候需要一些资源或者他的一个当时的操作系统的状态。就好像这儿有一个 M 主线程。在我们运行过程中,比如说在一个位置,需要开启一个携程,那么在携程起来的时候需要有一个商业的环境。就好像一个人一样,这个人是在什么地方生活得有空气,有水。一个一个连起来时候,它要依赖于当前操作系统还有这个内存的分配,以及还有没有可分配,这些都可以成为一个很重要的概念,就是他需要的资源和它的运行。
创建这个过程中,比如说在这个专业的运行过程,突然在这个这个地方,需要去开启一个携程,那么这个携程网还可以形成一个规定它可以形成一个对比,比如又需要开发一个中间的一个鞋城,这又算是一个节点,或者是图文的一个环境,那么从这又可以继续开启另外的其他的携程,同样也可以形成一个,形成一个对立,形成对立相对简单。
二,MPG 模式运行的状态1
当前程序有三个 M,如果三个 M 都在一一个 cpu 运行, 就是并发,如果在不同的 cpu 运行就是并行。
M1,M2,M3正在执行一个 G, M1的协程队列有三个,M2 的协程队列有3个,M3 协程队列有2个。
从上图可以看到: Go 的协程是轻量级的线程,是逻辑态的,Go 可以容易的起上万个协程。
其它程序 c/java 的多线程, 往往是内核态的,比较重量级,几千个线程可能耗光 cpu。
要理解这个事情首先得了解操作系统是怎么运用线程的。一个线程就是一个栈加一堆资源。操作系统一会让 cpu 跑线程 A,一会让 cpu 跑线程 B,靠 A 和 B 的栈来保存 A 和 B 的执行状态。每个线程都有自己的栈。
但是线程又贵,所以 go 发明了 goroutine. 大致就是说给每个 goroutine 弄一个分配在 heap 里面的栈来模拟线程栈。比如有3个 goroutine, A,B,C, 就在 heap 上开发出三个栈。然后 Go 让一个单线程的 scheduler 开始这三个栈。相当于{ A(); B(); C() },连续的,串行的跑。和操作系统不太一样的是,操作系统可以随时随地把线程停掉,切换到另一个线程。这个单线程的 scheduler 没有那个能力,它就是 user space 的一段朴素的代码,他跑 A 的时候控制权是在 A 的代码里面。A 自己不退出也没有其他办法。所以 A 跑一小段后需要主动主动反馈进行休息, 这时候可以看做A返回了。A 返回了 B 就可以跑了,然后 B 跑一小段后保存状态,返回,然后 C 再跑。C 跑一段返回。
这样跑完 A(); B(); C()}之后, 我们发现,好像他们都只跑了一小段。所以外面要包一个循环,大致是:goroutine_ list = [A, B, C
while (goroutine) :
for goroutine in goroutine_ list:
r = goroutine()
if r. finished():
goroutine_ list. remove(r)
当前程序有三个 M,三个 M 有三个 em,如果三个都运行在一个 cpu,就称之为并发。
概念第二点 M1M2M3正在执行一个。可以知道 M1现在正在执行一个 G。携程正在实行,同样M来执行一个 G,也在同时执行。
在 P 的地方,它可以开启 G,这种多的情况下,可以进行一个队列,这是它的一个模式。每个携程可以去完成它的一个任务。那么其他程序比如说 C,是多线程,线程往往是内核态的比较重量级的,几千个线程就可能耗光 cpu,耗费 cpu 这是他的第一种状态。
三,MPG 模式运行的状态2
分成两个部分来看。
原来的情况是 MO 主线程正在执行 G0 协程,另外有三个协程在队列等待。
如果 G0 协程阻塞,比如读取文件或者数据库等。
这时就会创建 M1主线程(也可能是从已有的线程池中取出 M1),并且将等待的3个协程挂到 M1下开始执行,M0 的主线程 下的 GO 仍然执行文件 io 的读写。
这样的 MPG 调度模式,可以既让 G0 执行,同时也不会让队列的其它协程一直阻塞, 仍然可以并发/并行执行。
等到 G0 不阻塞了,M0 会被放到空闲的主线程继续执行(从已有的线程池中取),同时 G0 又会被唤醒。
举一个动态的 MPG 运行的一个状态二。
比如现在的情况,分成两部分来看是 M0 M0,这就是主线程正在执行,等待时如果遇到了一个 go 携程被阻塞。什么情况下会堵塞这个一个携程,例如文件或者数据库,因为文件此时不知道要花多少时间,操作系统它就是这个携程,底层就会等待一个文件到底要多久并不知道,对计算机来说已经很长了,它认为在读文章的时候,cpu 有点浪费时间等,所以此时可能会做这样的事情 cpu 他们一个空闲状态就会创建一个叫 M1的,一个主题,也有可能这个事情已经从有了一个现场秩序,只有根据当时的情况,M0再运行,或者有可能维护一个线程池。
例如里面有可能预留一个,或者一可能没有,这个就跟操作系统底层相关联,如果有,就把这个 M 一个拿过来并进行唤醒,让它工作,如果没有,就出现这样的情况,跟操作系统底层有关系,跟上下文环境有关系。M 用就是原来进行等待的现在立即就来工作。并且将等待的三个携程挂到 M1下面去执行。其实本质上来说可能就是一个列表,也可能是一个队列的数据结构,完全可以做挂到,可以简单的认为让这个 M1指向。对于一个队列,我们可以把这个地址给他,就相当于挂到上面之后就开始执行,那么 F0这个主线程,叫做功能仍然执行这个,他原来的读写,例如 MPG 模式既可以正常的读文件,文件同时也不会让队列中的其他一直堵塞。此时这个东西,就有效的把 cpu 运行起来了,其他这样的情况,可以既保证主线程的一种进程,又能够保证形成一种并发和并行都能持续的状态。
比如跑完一圈 A, B, C 之后任何一个都没执行完,那么就在回到 A 执行一次。由于把 A 的栈保存在了 HEAP 里,此时就可以把A的栈复制粘贴会系统栈里(其实真实情况不是这样的,会意即可),然后再调用 A,这时候由于 A 是跑到一半自己说跳出来的,所以会从刚刚跳出来的地方继续执行。
图中看,有2个物理线程 M,每一个 M 都拥有一个 context(P),每一个也都有一个正在运行的 goroutine。
P 的数量可以通过 GOMAXPROCS()来设置,其实也就代表了真正的并发度,即有多少个 goroutine 可以同时运行。
图中灰色的那些 goroutine 并没有运行,而是出于 ready 的就绪态,正在等待被调度。P维护着这个队列(称之为 runqueue)
Go 语言里,启动一个 goroutine 很容易: go function 就行,所以每有一个 go 语句被执行,runqueue 队列就在其末尾加入一个
goroutine,在下一个调度点,就从 runqueue 中取出(如何决定取哪个 goroutine ? )一个 goroutine 执行。
为何要维护多个上下文 P?因为当一个 0s 线程被阻塞时,P 可以转而投奔另一个 OS 线程!
图中看到,当一个 0S 线程 MO 陷入阻塞时,P 转而在 os 线程M1I上运行。调度器保证有足够的线程来运行所以的 context P。