线程模型实现
goroutine是GO特有的应用程序线程。 有3个必知的核心元素,它们支撑起了这个模型的主要框架
- M:machine的缩写。一个M代表一个内核线程,或称“工作线程”
- P:processor的缩写。一个P代表一个GO代码片段所必须的资源(或称上下文环境)
- G:goroutine的缩写。一个G代表一个Go代码片段。
简单来说,一个G的执行需要P和M的支持。一个M在与一个P关联之后,就形成了一个有效的G运行环境(内核线程+上下文环境)。每个P都会包含一个可运行的G队列(runq)。该队列中的G会被一次传递给与本地P关联的M,并获得运行时机。
M与KSE(内核调度实体)之间总是一对一的关系。一个M能且仅能代表一个内核线程。Go的运行时系统用M代表一个内核调度实体。一个M在其生命周期内,会且仅会与一个KSE产生关联。相比之下,M与P,P与G之间的关联都是易变的,它们之间的关系会在实际调度的过程中改变。其中,M与P之间也总是一对一的,P与G之间则是一对多的关系。此外,M与G之间也会建立关联,因为一个G终归会由一个M来负责运行。它们之间的关联会由P来关联。
M
一个M代表一个内核线程。在大多数情况下,创建一个M都是由于没有足够的M来关联P并运行其中可运行的G。不过,在运行时系统执行系统监控或垃圾回收等任务的时候,也会导致新M的创建
type m struct{
g0 *g
mstartfn func()
curg *g
p puintptr
nextp puintprr
spinning bool
lockedg *g
.....
}
部分字段释义
g0表示一个特殊的goroutine。这个goroutine是Go运行时系统在启动之初创建的。用于执行一些运行时任务。字段mstartfn表示M的启始函数,这个函数其实就是我们在编写go的语句时携带的那个函数。字段curg会存放当前M正在运行的那个G的指针。而字段p的值则会指向与当前M相关连的那个P。字段nextp用于暂存与当前M有潜关联的P。把调度器将某个P赋给某个M的nextp字段的操作,称为对M和P的预联。运行时系统有时候会把刚刚重新启用的M和已与它预联的那个P关联在一起,这也是nextp字段的主要作用。字段spinning是bool类型的。它用于表示这个M是否正在寻找可运行的G,在寻找过程中,M会处于自旋状态。这也是该字段名的由来。Go运行时系统可以把一个M和一个G锁定在一起。一旦锁定,这个M就只能运行这个G,这个G也只能由该M运行。lockedg表示的就是与当前M锁定的那个G(如果有的话)。
M在创建之初,会被加入到全局的M列表。这时,他的其实函数和预联的P也会被设置。最后,运行时系统会为这个M专门创建一个新的内核线程并与之相关联。其中,起始函数仅当运行时系统要用此M执行系统监控或垃圾回收等任务的时候才会被设置。M的数量是可以设置的。
P
P是G能够在M中运行的关键。Go的运行时系统会适时地让P与不同的M建立或断开连接,以使P中那些可运行的G能够及时获得运行时机,这与操作系统内核在CPU之上实时的切换不同的进程或线程的情形类似。
P的数量是可以设置的。不过,设置P的最大数量只能限制住p的数量。而对G和M的数量没有任何约束。当M因系统调用而阻塞(更确切的说,是它运行的G进入了系统调用)的时候,运行时系统会把该M与P分离开来。以便其他的M重新关联P中未运行的G
与空闲M列表类似,运行时系统中也存在一个调度器的空闲P列表(runtime.sched.pidle)。当一个P不再与任何M关联的时候,运行时系统就会把它放入该列表;而当运行时系统需要一个空闲的P关联某个M的话,会从此列表中取出一个。注意,P进入空闲P列表的一个前提条件是它的可运行G列表必须为空,例如,在重整全局P列表的时候,P在被晴空可运行G队列之后,才会被放入空闲的P列表
P本身是有状态的
- Pidle。 当前P未与任务M存在关联
- running。 当前P正在与某个M关联
- Psyscall。 当前P中运行的那个G正在进行系统调用
- Pgcstop。 表明运行时系统需要停止调度。比如在开始垃圾回收的某些步骤前。
- Pdead。 表明P已经不会再被使用。如果在GO程序运行中,通过调用runtime.COMAXPROCES函数减少了P的最大数量,多余的P就会被置于此状态
每个P中除了都要一个可运行的G队列外,还都包含了一个自由G列表。这个列表包含了一些已经运行完成的G。当其增长到一定程度,运行时系统就会把其中的部分G转移到调度器的自由G列表中。另一方面,当使用go 语句欲启动一个G的时候,运行时系统会先试图从相应的P的自由G列表中获取一个现成的G,来封装这个go语句携带的函数(也称为go函数),仅当获取不到这样一个G的时候才需要创建一个G。考虑到由于相应P的自由G列表为空而获取不到自由的G的情况,运行时系统会在发现其中的自由G太少时,预先尝试从调度器的自由G列表中转移过来一些G。
G
一个G就代表一个goroutine,也与go函数相对应。Go的编译器会把go语句变成对内部函数newproc的调用,并把go函数及其参数都作为参数传递给这个函数。
运行时系统接到一个调用之后,会先检查go函数及其参数的合法性,然后试图从本地P的自由G列表和调度其的自由G列表获取可以的G,如果没有获取到,就新建一个G。与M和P相同,运行时系统也持有一个G的全局列表。新建的G会在第一时间被加入到该列表。该列表的作用是,集中存放当前运行时系统的所有的G的指针。无论用于封装当前这个go函数的G是否是新的,运行时系统都会对它进行一次初始化。包括关联go函数以及设置该G的状态和ID.