Skip to main content
 首页 » 编程设计

go中的线程的实现模型-P G M的调度

2022年07月19日122freeliver54

线程实现模型

go中线程的实现是依靠 P G M

M machine的缩写。一个M代表一个内核线程,或称工作线程

P processor的缩写。一个P代表执行一个Go代码片段所需要的资源(或称上下文环境

G goroutine的缩写。一个G代表一段Go代码片段。前者是对后者的一种封装。

 

可能存在的调度关系

 

1、M

 一个M代表一个内核线程。在大多数情况下,创建一个M,都是由于没有足够的M来关联P并运行其中可运行的G。不过,在运行时系统执行系统监控或垃圾回收等任务的时候,也会导致新M的创建。下面是M的部分结构。

 

上面是M中重要的几个字段。

1g0标识一个特殊的goroutine。这个goroutineGo运行时系统在启动之初创建的,用于执行一些特殊的额任务。

2mstartrn标识M的起始函数,这个函数就是我们在编写go语句携带的那个函数。

3curg会存储当前M正在运行的那个G的指针

4p额值会指向与当前M相关联的那个P

5nextp字段用于暂存与当前M有潜在关联的P。把调度器将某个P赋值给某个Mnextp字段的操作称为MP的颈联。运行时系统有时候会把刚刚重新启用的M和已与他颈联的那个P关联在一起,这也是nextp字段的主要作用。

6、字段spinningbool类型,它表示这个M是否正在寻找可运行的G。在寻找过程中,M会处于自旋的状态。

 

M在创建之初就会加入到全局(runtime.allm)的M列表。这时,它的起始函数和预联P也会被设置。最后,运行时系统会为这个M专门创建一个新的内核线程与之相关联。起始函数仅当运行时系统要用此M执行系统监控或垃圾回收等任务的时候才会被设置。全局M列表,运行时系统需要他的时候,会通过它获取所有M的信息。同时,它也可以防止M被当做垃圾回收掉。

 

在新的M被创建之后,Go运行时系统会对他进行一番初始化,其中包括对自身所持的站空间以及信号处理方面的初始化。在这些初始化工作都完成之后,该M的起始函数会执行(如果存在的话)。如果,这个起始函数代表的是系统监控任务的话,那么该M会一直执行它,而不会继续后面的流程。否则,在起始删除执行完毕之后,当前M将会与那个预联的p完成关联,并准备执行其他的任务。M会依次在多处寻找课运行的G并运行。

 

运行时系统管辖下的M(或者runtime.allm中的M)有时候也会被停止,比如在运行时系统执行垃圾回收任务的过程中。运行时系统正在停止M的时候,会把它放入调度器的空闲M列表(runtime.sched.midle)。在需要一个未被使用的M的时,运行系统会先尝试从该队列中获取。M是否空闲,仅以它是否存在于调度器的空闲M列表中为依据。

 

单个GO程序使用的M的数量是可以设置的。go启动的时候会先启动一个引导程序,这个引导程序会为其创建必要的环境。其中包括M的最大值,初始值是1000。但是实际情况这么多的线程是很难共存的。

 

新设置的值不能小于正在运行m的个数。

2、P

Go的运行时系统会适时的让P与不同的M建立或断开关联,以使p中的那些可运行的G能够获得运行时机,这与操作系统内核在cpu之上实时的奇幻不同的线程或进程的情况类似。

 

改变单个Go程序间接拥有的P的最大量有两种方法。1、调用函数runtime.GOMAXPROSS并把想要设定的数量作为参数传入。2、运行前设置环境变量GOMAXPROSS的值。(这个值一般初始化设定是cpu的核数)但是我们需要注意的尽量在go初始化的时候进行设置。一个G被启动后,会被追加到某个P的可运行的G队列中。如果当前M因为系统调用而阻塞(更确切的说,是它运行的G进入了系统调用)的时候,运行时系统会把M和它关联的P分离开。如果这个P的可运行的队列中还有可执行的G,那么运行时系统会找到一个空闲的M,或创建一个新的M,并和P关联。因此,有很多时候M的数量是大于P的数量的。  

 

确定完成P最大量之后,运行时系统会根据这个数值重整全局的P列表(runtime.allp)。于全局的M;列表类似,该列表中包含了当前运行时系统创建的所有P。运行时系统会把这些P中可运行的G全部取出,并放入调度器的可运行G队列中。这是调整全局全局P列表的一个重要条件。被转移的哪些G,会在以后经由调度再次放入某个P的可运行G队列。

 

与空闲M列表类似,运行时系统也存在一个调度器的P列表(runtime.sched.pidle)。但一个P不在于任何M关联的时候,运行时系统就会把它放到该列表;而当运行时系统需要一个空闲P关联任何M的时候,会从次列表中取出一个。P进入空闲P列表的一个前提条件是它的可运行的G队列必须为空。

 

M不同的是P是有状态的

 

1Pide 此状态表示当前P未与任何的M存在关联

2Prunning 此状态表示当前P正在与某个M关联

3Psyscall   此状态表示当前P的运行的那个G正在进行系统的调用。

4Pgcstop   此状态表明运行时系统需要停止调度。例如,运行时系统在开始垃圾回收的某步骤前,就会试图把全局的P列表中的所有P都至于此状态。

5Pdead  此状态表明当前P已经不会在被使用,如果Go程序运行的过程中,通过runtime.GOMAXPROCS函数减少了P的最大量,那么多余的P就会被运行时系统至于此位置。

 

 

P在创建之初的状态是Pgcstop。持续的时间很短暂,马上就会变成Pidle

 

每个P中除了都有一个可运行的G队列外,海都包含了一个自由G列表。这个列表中包含了一些已经运行完成的G。随着运行完成的G的增多,该列表会很长。如果它增加到一定的长度,运行时系统就会把其中的部分G转移到调度器的自由G列表中。另一方面,当使用go语句欲启动一个G列表的时候,运行时系统会先试图从相应P的自由列表中获取一个现成的G,来封装这个Go语句携带的函数(也称GO函数),仅当获取不到这样的一个自由G的时候,运行时系统会在发现其中的自由G太少时,预先尝试从调度器自由G列表中转译过来一些G。所以,只有在自由G列表页完全为空的情况下,才会有新的G被创建。这样大大提高了G的复用率。

3G

一个G代表的是一个goroutine

Go的编译器会把go语句变成内部函数newproc的调用,并把go函数及其内部的参数传递给这个函数。

 

运行时系统接到这样的一个调用之后,会先检查go函数及其参数的合法性。然后视图从本地的P自由G列表和调度器的自由G列表获取可用的G。如果没有获取到,就新建一个G。与MP相同,运行时系统持有一个G的全局列表。新建的G会第一时间加入到这个列表中。这个全局的列表的主要的作用:集中存放当前运行时系统中所有的G的指针。无论用于封装的这个GO函数的G是否全新,运行时系统都会对他进行一次初始化,包括go函数以及设置G的状态和ID等步骤。在初始化完成之后,这个G会立即被存储到本地Prunnext字段中,该字段用于存储刚出炉的G,以便于更好的运行它。如果这时runnext字段已经存有一个G,那么这个已有的G就会被放到该P的可运行G队列的末尾。如果该队列已满,那么这个G就只能追加到调度器的可运行G队列中了。

1Gidle 表示G刚被新分配,但是还没有初始化。

2Grunnable 表示G正在课运行队列中等待运行。

3Grunning  表示G正在运行。

4Gsyscall    表示G正在执行某个系统调用。

5Gwaiting    表示G正在阻塞。

6Gdead        表示G正在闲置

7Gcopystack  表示当前G的栈正在被移动,移动的原因可能是栈的扩展或收缩。

 

在运行的时候,我们用G封装Go函数的时候,会先对这个G进行初始化。一旦G准备就绪,其状态

就会被设置成Grunnable。一个G真正被使用的时候是在其状态被设置成Grunnable之后。

 

一个G在运行的过程中,是否会等待某个事件以及会等待什么样的事件,这个是由封装的Go函数决定的。

比如说:

1、其中的函数对通道的操作,包括对通道值的接收和发送,那么到对应的代码有可能进入Gwiting2、涉及网络I/O的时候也会导致

3、操控定时器(time.Timer)和调用time.Sleep函数同样也会造成G的等待。在事件到来之后,G会被唤醒到Grnnable状态。然后等待被调度执行。

 

 

G在退出系统调用的时候。运行器先尝试直接运行这个G,仅当无法直接运行的时候,才会吧它装换成Grunnable状态发放入到调度器的自由G列表中。 这样在其退出系统调用的时候就立刻被运行大大提高了运行的效率。

 

进入死亡状态(Gdead)的G是可以重新初始化并使用的,但是P进入死亡状态(Gdead)之后就只能被销毁了。

调度器

一轮调度

引导程序会为Go建立必要的运行环境。完成了初始化的工作之后,Go程序中的main函数才会执行。

然后会让封装Main函数的G马上有机会运行。封装main函数的G总是Go运行时系统创建的第一个用户G。所以这个G总是最先执行的。(通过main函数创建的G如果一起执行的话,不一定会运行)

 

一轮调度的时候回判断M是否已经和G锁定。如果发现当前M已经和M锁定了,就会暂时阻塞当前的M。一旦与他锁定的G处于可运行的状态,它就会唤醒阻塞的那个G,然后继续运行。那么阻塞当前M意味着相关的内核线程不能在去做其他的事情了。调度器也不会为当前M寻找可运行的G,相当于在浪费资源。如果调度器为当前M找到了一个可运行的G,但发现这个G已经和某个M锁定了,他回去唤醒与之锁定的M易云星该G,并重新为当前M寻找可运行的G

 

如果判断当前M未与任何的G锁定,那么一轮调度的主流程就会继续运行。这时候,调度器会检查运行时系统是否有运行的串行任务正在等待执行。(串行任务的执行需要停止Go的调度器个人猜测串行的执行是一步一步执行,并发的执行会破坏串行的资源)如果有串行的任务,需要停止调度器。字段gcwaitingstopwaitstopnote都是串行运行时任务执行前后的辅助协调手段。gcwaiting表示是否需要停止调度,在停止之前这个值会被设置为1;再恢复调度之前,该值会被设置为0。这样主要调度器发现gcwaiting的值为1,就会把P的状态设置为Pgcstop,然后自减stopwait字段的值。如果发现自减后的值为0,就说明所有P的状态都已为Pgcstop。这时候就可以利用stopnote字段,唤醒所有等待中的M

 

Go调度并不是运行在某个专用的内核线程中的程序,调度程序会运行在若干已存在的M(或者内核线程)中。调度的时候运行系统中几乎所有的M都会参与调度任务,它们共同实现了Go调度器的功能。

全力查找可运行的G

调度器如果没有发现可运行的G的时候就会进入全力查找可运行G”的子流程。概括的说就是,这个子流程会多次尝试从各处搜索可运行的,甚至从别的P红偷取可运行的G

1、获取执行终结器的G

2、从本地P的可运行G队列获取G

3、从调度器的可运行G队列获取G

4、从网络I/O轮训器(或称netpoller)处获取G

5、从其他P的可运行G队列获取G

6、获取GC标记任务的G

7、从调度器的可运行G队列获取G

8、从全局P列表中每个P的可运行G队列获取G

 

网络I/O轮训器(即netpoller)是Go为了操作系统提供异步I/O基础组件之上,实现自己的阻塞式I

/O而编写的子程序。Go所选用的异步I/O基础组件都是可以高效执行网络I/O(比如epollkqueue

当一个G视图在一个网络连接上进行读、写操作的时候,底层程序(包括基础组件)就会开始为此做准备,此时G就会被转入Gwaiting状态。一旦准备好了,基础组件就会返回相应的事件,就会让netpoller立即通知为此等待的G(?是不是阻塞的G都会进入Gwaiting呢)

 

G的自旋,当M还没有找到G来运行。直到找到了可运行的G,或者始终未找到G而需要停止M,当前M就会退出自旋的状态。一般情况下,运行时系统中至少会有一个自旋的M,调度器会尽量保证有一个自旋的M存在。

启动或停止M

1stopm()。停当前M的执行,直到有心的G变的可运行而被唤醒。

2gcstopm()。为串行运行时任务的执行让路,停止当前M的执行。串行运行时任务执行完毕之后会被唤醒。

3stoplockedm()。停止与某个G锁定的当前M的执行。直到整个G变的可运行而被唤醒、

4startlockedmgp *g)。唤醒与gp锁定的那个M,并让该M去执行gp

5startm_p_ *p,spinning bool)。唤醒或创建一个M区关联_P_并开始执行。


本文参考链接:https://www.cnblogs.com/ricklz/p/10953173.html
阅读延展