Go语言GMP模型
进程、线程、协程
- 进程:进程是系统进行资源分配的基本单位,有独立的内存空间,单切换代价极高,进程间通信也比较麻烦
- 线程:线程是CPU调度和分派的基本单位,线程依附于进程,与其他线程共享进程的资源,仅有自己的(程序计数器,一组寄存器的值,和栈),线程切换代价小(但是线程之间的切换可能会设计用户态和内核态的切换),由于共享进程资源,所以线程之间通信比较方便。
- 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程切换只需要保存和恢复任务的上下文,没有内核的开销。协程间通信也比较简单(协程间本身是不可抢占的,由于操作系统的调度机制无法影响到它,因此一般存在用户自定义的调度机制)(也可以这么说内核线程依然叫“线程(thread)”,用户线程叫“协程(co-routine)".)
线程的代价 - 使用过C++/Java的同学应该都知道在做服务端开发的时候,对于多线程的使用,繁琐程度,以及对于多线程情况下异常情况的分析,是何等头疼,如果线上流量过大,对于这种多线程的操作更是得慎重,当然对于机器资源的消耗也是随之增加的,其实对于程序员来说最主要就是简单,高效~
- 空间大小:一般情况下一个thread的创建大小默认占用栈空间1M,而goroutine默认则是2kb
- 切换开销:thread的切换是需要通过用户态达到内核态,会设计到上下文的切换开销,对应资源的耗费会很大
- 线程通信:常见的线程通信有三种方式(C++),使用起来相对比较复杂,最棘手的问题就是共享内存,一般会使用锁,使用到锁,就会引申各种锁的问题,比如死锁。
- 资源回收:创建线程相对简单,回收线程资源会麻烦很多,常见有detached和join俩种方式,对于无法大量创建线程的时候,会考虑多路复用方式,这样的麻烦就是另个层次了。
GMP模型
- G:Goroutine,每个Gotoutine对应一个G结构体,G存储Goroutine的运行堆栈,状态,以及任务函数,可重用(函数实体)G需要保存到P才能被调度执行
- M:machine,os内核线程抽象,代表真正执行计算的资源,在绑定有效的P后,进入schedule循环;而shcedule循环的机制大致是从Global队列,P的local队列以及wait队列中获取。
M的数量是不固定的,有Go Runtime调整,为了防止创建过多OS线程导致系统调度不过来,目前默认设置为10000个,M不保存G的上下文,这是G可以跨M的基础。 - P:Processor,表示逻辑处理器,对G来说,P相当于CPU核,G只有绑定到P才能被调度。对M来说,P提供了相关的执行环境,入内存分配状态,任务队列等。
P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量)。
P 的数量由用户设置的 GoMAXPROCS 决定,但是不论 GoMAXPROCS 设置为多大,P 的数量最大为 256。 - 调度器循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复。
可以通过经典的地鼠推车搬砖的模型来说明其三者关系: - 地鼠(Gopher)的工作任务是:工地上有若干砖头,地鼠借助小车把砖头运送到火种上去烧制。M 就可以看作图中的地鼠,P 就是小车,G 就是小车里装的砖。
Processor(P):
根据用户设置的 GoMAXPROCS 值来创建一批小车(P)。
Goroutine(G):
通过 Go 关键字就是用来创建一个 Goroutine,也就相当于制造一块砖(G),然后将这块砖(G)放入当前这辆小车(P)中。
Machine (M):
地鼠(M)不能通过外部创建出来,只能砖(G)太多了,地鼠(M)又太少了,实在忙不过来,刚好还有空闲的小车(P)没有使用,那就从别处再借些地鼠(M)过来直到把小车(P)用完为止。
这里有一个地鼠(M)不够用,从别处借地鼠(M)的过程,这个过程就是创建一个内核线程(M)。
需要注意的是:地鼠(M) 如果没有小车(P)是没办法运砖的,小车(P)的数量决定了能够干活的地鼠(M)数量,在 Go 程序里面对应的是活动线程数;
调度过程
• 首先创建一个G对象,G对象保存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。
• M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
参考
• https://www.it610.com/article/1293176199840866304.htm
• https://studygolang.com/articles/27934?fr=sidebar