Go并发编程-线程模型(M)

2020-09-15  本文已影响0人  筑梦之队

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


M的部分数据结构

M结构中的字段众多,这里只挑选了几个最重要的字段。其中:
g0:表示一个特殊的goroutine。这个goroutine是Go运行时系统在启动之初创建的,用于执行一些运行时任务。
mstartfn:表示M的起始函数,这个函数其实就是我们在编写go语句时携带的那个函数。
curg:存放当前M正在运行的那个G的指针。
p:指向与当前M相关联的那个P。
mstartfn、curg和p最能体现当前M的即时情况。
此外,字段nextp用于暂存与当前M有潜在关联的P。让调度器将某个P赋给某个M的操作由nextp字段控制,称为对M和P的预联。运行时系统有时候会把刚刚重新启用的M和已与它预联的那个P关联在一起,这也是nextp字段的主要作用。
字段spinning是bool类型的,它用于表示这个M是否正在寻找可运行的G。在寻找过程中,M会处于自旋状态。这也是该字段名的由来。
Go运行时系统可以把一个M和一个G锁定在一起。一旦锁定,这个M就只能运行这个G,这个G也只能由该M运行。标准库代码包runtime中的函数LockOSThread和UnlockOSThread,也为我们提供了锁定和解锁的具体方法。M的字段lockedg表示的就是与当前M锁定的那个G(如果有的话)。

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

在新M被创建之后,Go运行时系统会先对它进行一番初始化,其中包括对自身所持的栈空间以及信号处理方面的初始化。在这些初始化工作都完成之后,该M的起始函数会被执行(如果存在的话)。注意,如果这个起始函数代表的是系统监控任务的话,那么该M会一直执行它,而不会继续后面的流程。否则,在起始函数执行完毕之后,当前M将会与那个预联的P完成关联,并准备执行其他任务。M会依次在多处寻找可运行的G并运行它。这一过程也是调度的一部分。有了M,Go程序的并发运行基础才得以形成。

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

单个Go程序所使用的M的最大数量是可以设置的。Go程序运行的时候会先启动一个引导程序,这个引导程序会为其运行建立必要的环境。在初始化调度器的时候,它会对M的最大数量进行初始设置,这个初始值是10,000。也就是说,一个Go程序最多可以使用10,000个M。这就意味着,最多可以有10,000个内核线程服务于当前的Go程序。请注意,这里说的是最理想的情况;由于操作系统内核对进程的虚拟内存的布局控制以及大小限制,如此量级的线程可能很难共存。从这个角度看,Go本身对于线程数量的限制几乎可以忽略。

相关链接:
Go并发编程-线程模型
Go并发编程-线程模型(M)
Go并发编程-线程模型(P)
Go并发编程-线程模型(G)

参考资料:
https://www.ituring.com.cn/book/tupubarticle/16048

上一篇下一篇

猜你喜欢

热点阅读