Go语言并发模型 M(Machine)

M 即 machine,一个 M 代表一个内核线程,或者称之为 “工作线程”。

M 在创建之初会被加入到全局的 M 列表中。创建一个 M 的原因是由于没有足够的 M 来关联 P,并运行其中可运行的 G。另外,当运行时系统执行监控或者垃圾回收的时候也会创建新的 M 。

M 的部分结构如图所示:

Go语言并发模型 M(Machine)

M 中最重要的当属如下四个字段:

  • g0 表示一个特殊的 goroutine。这个 goroutine 是 Go 运行时系统在启动之初创建的,用于执行一些运行时任务。
  • curg 存放当前 M 正在运行的那个 G 的指针。
  • p 指向与当前 M 相关联的 P。
  • mstartfn 表示 M 的起始函数,就是我们在编写 go 语句时携带的那个函数。
  • nextp 存放当前 M 有潜在联系的 P(预联)。
  • spinning 存放一个 bool 值,用于表示这个 M 是否正在寻找可运行的 G。在寻找过程中,M 会处于自旋状态。

mstartfn、curg 和 p 体现了当前 M 的即时情况。Go 运行时系统可以把一个 M 和一个 G 锁定在一起。一旦锁定,这个 M 就只能运行这个 G,这个 G 也只能由该 M 运行。标准库代码包 runtime 中的函数 LockOSThread 和 UnlockOSThread,为我们提供了锁定和解锁的具体方法。M的字段 lockedg 表示的就是与当前 M 锁定的那个 G。

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 的最大数量进行初始设置,这个初始值是 10000。也就是说,一个 Go 程序最多可以使用 10000 个 M。这就意味着,最多可以有 10000 个内核线程服务于当前的 Go 程序。请注意,这里说的是最理想的情况;由于操作系统内核对进程的虚拟内存的布局控制以及大小限制,如此量级的线程可能很难共存。从这个角度看,Go 本身对于线程数量的限制几乎可以忽略。

除了上述初始设置之外,我们也可以在 Go 程序中对该限制进行设置。为了达到此目的,你需要调用标准库代码包 runtime/debug 中的 SetMaxThreads 函数,并提供新的 M 最大数量。runtime/debug.SetMaxThreads 函数在执行完成后,会把旧的 M 最大数量作为结果值返回。非常重要的一点是,如果你在调用 runtime/debug.SetMaxThreads 函数时给定的新值比当时 M 的实际数量还要小,运行时系统就会立即引发一个运行时 panic。

P 是 G 能够在 M 中运行的关键。Go 的运行时系统会适时地让 P 与不同的 M 建立或断开关联,以使 P 中的那些可运行的 G 能够及时获得运行时机,这与操作系统内核在 CPU 之上实时地切 ...