先来理解一下,进程、线程、协程
- 进程:应用程序运行时的抽象
- 线程:依赖于进程,是线程是进程执行的最小单元,一个进程有多个线程
- 协程:协程是用户态的轻量级的线程
进程、线程为内核态下的,协程为用户态下的。
现在来看看多进程和多线程:
多进程:相互独立,进程与进程之间不会共享数据。举个例子,我需要去采集一个网站的所有数据,我写了一个程序去执行,这样采集的执行效率太低,这个时候我就多启动几个独立的进程去采集数据。
多线程:一个进程内部可以有多个线程,他们共享资源,共享内存空间。还是以刚刚的例子为演变,我启动一个进程去采集网站数据,这个时候启动多线程去操作,每个线程去采集不同的页面。
接下来看看GMP:
G:顾名思义就我们开发中常用的goroutine协程、需要执行的任务
M:machine,操作系统的线程,是执行任务的线程
P:processor:调度器(处理器)负责安排多个任务之间的调度
这里需要明确,如果要执行G任务,那么就需要在M上去执行,P需要安排和调度GM的。这里P需要绑定M才可以执行G任务,就是G的任务需要M的参与,G必须在M上才能执行。
从宏观的角度去看GMP:
首先是G就是我们常用goroutine,他是怎么执行的呢?首先在整个程序中一个全局队列sched.runq也是用来存放需要执行的G任务。
P则是调度器,逻辑处理器。主要负责调度G --> M去执行,前面也说过,如果要执行G任务就需要M的参与,需要在M上执行,这里就形成了G的任务要执行需要在M上才能运行,但是G到M这个过程能就需要P去调度(牵线搭桥)。P时有一个本地队列的runq,这个本地队列也是用来存储G的。所以即然你G任务执要执行的前提条件是在M上,可是M要去哪找G任务执行呢,这个时候就需要P调度器来完成了。
在整个程序运行的过程中P的数量是由GOMAXPROCS来决定的,也可以通过runtime包下的GOMAXPROCS()去设置。
接下来看看M,首先我们要知道M为操作系统线程,也就是任务线程,用来执行任务使用的,他如果要执行G任务就要和P绑定。
M去调度执行G任务的时候,首先先从P的本地队列runq获取可执行的G,如果没有就去全局队列sched.runq中获取,如果没有就去wait队列中获取,确保不会空闲。还有一种情况就是P本地队列没有可执行的G会去其他的P那里窃取一半的G放入自己的本地队列执行,这就是work-stealing机制。
P与M的数量:
P的数量一开始就是GOMAXPROCS来决定的,也可以runtime.GOMAXPROCS来设置。M是由runtime动态创建的,go底层会尽量复用空闲的M,空闲时间长的M会被销毁。这里就是当某个M不再绑定P了,M变得空闲,这个时候这个空闲的M就会被存放一个sched.idlem的队列中等待,下次需要M的时候从队列中获取,这样去复用。如果队列过长或者长时间不使用的M将会被销毁。
还是看看源码吧:
G:
type g struct {
m *m
sched gobuf
atomicstatus atomic.Uint32
.....
}
type gobuf struct {
sp uintptr // 保存 CPU 的 rsp 寄存器的值,指向函数调用栈栈顶(指向栈指针)
pc uintptr // 保存 CPU 的 rip 寄存器的值,指向程序下一条执行指令的地址(程序计数器)
ret uintptr // 保存系统调用的返回值
bp uintptr // 保存 CPU 的 rbp 寄存器的值,存储函数栈帧的起始位置.
g guintptr // 当前G
}
这里g的字段多,还是针对性的讲讲吧。
- m:这里的m就是你想的那个m就是这个G需要在哪个M上去执行。
- 这里就是goroutine正在运行中,调度器P就会设置h.m = 当前的M
- 如果goroutine如果发生阻塞时,调度器就会将h.m = nil
- atomicstatus:表示当前G的状态(枚举值)
- _Gidle = 0 为协程开始创建的状态,此时尚未初始化完成
- _Grunnable = 1 协程在等待执行队列中,等待被执行
- _Grunning = 2 协程正在运行,同一时刻一个p中只有一个g处于当前状态
- _Gsyscall = 3 协程正在执行系统调用(阻塞于系统调用中)
- _Gwaiting = 4 协程被挂起状态,等待被唤醒 如channel、锁、select 等
- _Gdead = 6 协程调用结束资源可以回收
- _Genqueue = 7 被放入全局队列中
- _Gcopystack = 8 协程正在扩容中(正在复制 stack)
- _Greempted = 9 协程被抢占后的状态
M:
type m struct {
g0 *g // goroutine with scheduling stack
tls [tlsSlots]uintptr // thread-local storage (for x86 extern register)
curg *g // current running goroutine
p puintptr // attached p for executing go code (nil if not executing go code)
}
- g0:先讲讲这个,这个是一个特殊的调度协程,每个M都会有一个g0
- tls:线程本地存储,存储内容只对当前线程可见
- curg:当前正在运行的G
- p:当前绑定的 P(Processor)
现在我们来讲讲两个比较特殊的东西G0和M0:
- G0:
- 每一个M都会对应有一个G0 1:1
- 执行固定的调度流程
- 负责执行用户函数普通的g
- 切换goroutine的时候g0就负责记录上下文信息(寄存器、栈指针)
- 执行过程 M 执行 G1 → 切到 G0 → 调度出 G2 → 切到 G2 执行 → …… 依此类推。
- M0:进程启动的第一个线程(M),对应主线程
- 用于初始化和操作第一个G