这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天, 今天主要学习Go的GMP模型。
GMP概念:
G: GO运行时对goroutine的描述,G中存放并发执行的代码入口地址、上下文、运行环境(关联的P和M)、运行栈等执行相关的信息。G的新建、休眠、恢复、停止都受到Go运行时的管理。
GO运行时的监控线程会监控G的调度,G不会长久地阻塞系统线程,运行时的调度器会自动切换到其他G上运行。G新建或恢复时会添加到运行队列,等待M取出并运行。
M: OS内核线程,是操作系统层面调度和执行的实体。M仅负责执行,M不停地被唤醒或创建。然后执行。
P: 代表M和G所需要的资源,是对资源的一种抽象管理,P不是一个段代码实体,而是一个管理的数据结构,P主要是降低M对G的复杂性,增加一个间接的控制层数据结构。P控制GO代码的并行度,它不是实体。
在main函数进入之前的准备
sysmon
在main.main执行之前,Go语言的runtime库会初始化一些后台任务,其中一个任务就是sysmon。
sysmon会根据系统当前的繁忙程度睡一小段时间,然后每隔10ms至少进行一次epoll并唤醒相应的goroutine。同时,它还会检测是否有P长时间处于Psyscall状态或Prunning状态,并进行抢占式调度。
scavenger
newproc创建一个goroutine,第一个参数是goroutine运行的函数。scavenger的地位是没有sysmon那么高的——sysmon是由物理线程运行的,而scavenger只是由goroutine运行的。 接下来的章节会说明goroutine与物理线程的区别。
调度器
结构体G
struct G
{
uintptr stackguard; // 分段栈的可用空间下界
uintptr stackbase; // 分段栈的栈基址
Gobuf sched; //进程切换时,利用sched域来保存上下文
uintptr stack0;
FuncVal* fnstart; // goroutine运行的函数
void* param; // 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取
int16 status; // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; // goroutine的id号
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被锁定只能在这个m上运行
uintptr gopc; // 创建这个goroutine的go表达式的pc
...
};
上下文其实只保存了当前栈指针,程序计数器,以及goroutine自身。
struct Gobuf
{
// The offsets of these fields are known to (hard-coded in) libmach.
uintptr sp;
byte* pc;
G* g;
...
};
结构体M
struct M
{
G* g0; // 带有调度栈的goroutine
G* gsignal; // signal-handling G 处理信号的goroutine
void (*mstartfn)(void);
G* curg; // M中当前运行的goroutine
P* p; // 关联P以执行Go代码 (如果没有执行Go代码则P为nil)
P* nextp;
int32 id;
int32 mallocing; //状态
int32 throwing;
int32 gcing;
int32 locks;
int32 helpgc; //不为0表示此m在做帮忙gc。helpgc等于n只是一个编号
bool blockingsyscall;
bool spinning;
Note park;
M* alllink; // 这个域用于链接allm
M* schedlink;
MCache *mcache;
G* lockedg;
M* nextwaitm; // next M waiting for lock
GCStats gcstats;
...
};
这里也是截取结构体M中的部分域。和G类似,M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下,G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache,是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量,代表当前的M。同时存在多个M,表示同时存在多个物理线程。
结构体M中有两个G是需要关注一下的,一个是curg,代表结构体M当前绑定的结构体G。另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。
结构体P
struct P
{
Lock;
uint32 status; // Pidle或Prunning等
P* link;
uint32 schedtick; // 每次调度时将它加一
M* m; // 链接到它关联的M (nil if idle)
MCache* mcache;
G* runq[256];
int32 runqhead;
int32 runqtail;
// Available G's (status == Gdead)
G* gfree;
int32 gfreecnt;
byte pad[64];
};
注意,跟G不同的是,P不存在waiting状态。MCache被移到了P中,但是在结构体M中也还保留着。在P中有一个Grunnable的goroutine队列,这是一个P的局部队列。当P执行Go代码时,它会优先从自己的这个局部队列中取,这时可以不用加锁,提高了并发度。如果发现这个队列空了,则去其它P的队列中拿一半过来,这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。