Go语言GMP并发模型详解

179 阅读3分钟

GMP模型

G是goroutine,也就是用户用go关键字执行的用户自己编写的一个函数,每次go调用的时候,都会创建一个G对象。

M是线程,所有的G任务都是落地到线程上执行,每一个运行的M(注意是运行的M)都会绑定一个P。也就是说线程M想要运行任务G,先要获取一个P,从P的本地队列获取G,或者从全局队列或其他P的本地队列拿一批G放到当前P的本地队列然后去运行G。

P是线程处理器调度器,P调度器的作用是把可运行的goroutine分配到线程M上。每个P对应一个本地G任务队列和全局G任务队列。P的数量是由启动时环境变量$GOMAXPROCS或者GOMAXPROCS()决定

image-20240503103252236.png

注意:

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以即使P的默认数量是1,也有可能会创建很多个M出来。

但是一个M要想运行一个G,一定会对应一个P

go func()调度流程

image-20230416110405394.png

全局队列存放等待运行的G

P的本地队列和全局队列类似,也是存放等待运行的G,新建G时(go func())优先加入P的本地队列,如果满了就放到全局队列里

P有多个,通常一个P对应一个M,是将G分配给对应的这个M去调度执行

M线程想运行任务G就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。

M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

当M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;

当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到 P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中。

GMP模型为什么要有P

如果没有P,M要想执行或者放回G,都必须访问全局G队列,并且M有多个,也就代表着M对全局G队列的访问需要用到锁来保证同步互斥。造成激烈的锁竞争。

另一方面,如果一个M上运行的G创建了G',那么这个G'因为和G有相关关系,最好是交给当前M,而不是另外一个M'来执行,全局G队列来保证这个条件比较复杂。

在存在syscalls的情况下,线程经常被阻塞和解阻塞。这增加了很多额外的性能开销。如果有P,可以直接把P附带的这一个G队列赋给另一个空闲的M去处理了。

为什么P的逻辑不直接加在M上

M是线程,线程是内核的概念。而协程的调度是用户程序,内核并没有协程的概念。