GMP模型概述

GMP模型是Go语言的并发调度模型,它是由Goroutine、M(OS线程)和P(处理器)三个主要组件构成的。这个模型是Go运行时(runtime)用来调度Goroutines执行的机制,它允许数以万计的Goroutines能够在有限数量的线程上高效运行。下面是GMP模型各个组件的详细说明:

Goroutine(G)

Goroutine是Go语言中的轻量级线程,它是并发执行的实体。与OS线程相比,Goroutines的创建和销毁成本要低得多,它们占用的内存也更少,通常只有几KB。Goroutines在逻辑上是独立的,但实际上是由M来执行的。

M(Machine或OS线程)

M代表Machine,实际上就是操作系统的线程。Go运行时会在物理或虚拟处理器上创建一定数量的线程,这些线程用于执行Goroutines。每个M都会绑定一个P,然后从P的本地运行队列中获取Goroutine来执行。如果M阻塞了(例如,等待I/O操作),它会释放P,然后运行时会创建或唤醒另一个M来绑定到P上,以保持CPU的利用率。

P(Processor或处理器)

P代表Processor,它是Goroutine执行所需的资源的抽象。P的数量通常由GOMAXPROCS环境变量控制,它决定了系统同时可以有多少个Goroutines在运行。每个P都有一个本地的Goroutine队列,它负责维护等待运行的Goroutines。P的作用是将Goroutines分配给M来执行,它是M和G之间的调度器。

GMP模型的工作原理

  1. 初始化:程序启动时,Go运行时会根据GOMAXPROCS的值创建相应数量的P,并创建一些M来服务这些P。
  2. 执行Goroutines:每个P会从其本地队列中取出一个Goroutine,并将其分配给一个M来执行。
  3. 全局队列和本地队列:如果一个P的本地队列为空,它会尝试从全局队列中获取Goroutines,或者从其他P的本地队列中“偷取”一半的Goroutines。
  4. 阻塞和唤醒:如果一个M在执行Goroutine时被阻塞,它会释放P,并进入休眠状态。运行时会创建或唤醒另一个M来接管P。当阻塞的M完成等待的操作后,它会尝试获取一个空闲的P来继续执行Goroutines;如果没有空闲的P,它会将Goroutine放入全局队列,并进入休眠状态。

M与P

绑定时机

在Go语言的GMP模型中,M(Machine)与P(Processor)的绑定是由Go运行时进行管理的。绑定过程是在M需要执行Goroutines时发生的。以下是M与P绑定的一般过程:

  1. 启动时绑定:当Go程序启动时,Go运行时会根据GOMAXPROCS的值创建相应数量的P。同时,运行时也会创建一些M来服务这些P。每个M在启动时会尝试获取一个P,如果获取成功,M就与这个P绑定。
  2. 执行Goroutines:一旦M与P绑定,M就可以从P的本地运行队列中取出Goroutines来执行。如果本地队列为空,M(通过P)可以尝试从全局队列获取Goroutines,或者从其他P的本地队列中“偷取”Goroutines。
  3. 阻塞和解绑:如果M在执行Goroutine时被阻塞(例如,等待系统调用),它会释放P,这样其他的M就可以使用这个P来执行Goroutines。释放P的过程实际上是M与P的解绑。
  4. 唤醒和重新绑定:当M完成阻塞操作并被唤醒时,它会尝试重新获取一个P。如果有空闲的P,M将与之绑定并继续执行Goroutines。如果没有空闲的P,M可能会将其Goroutine放入全局队列,并进入休眠状态,直到有可用的P。
  5. 工作窃取:为了保持所有的M都在工作,Go运行时会使用工作窃取算法来平衡不同P的负载。如果一个M通过其绑定的P无法找到可执行的Goroutine,它会尝试从其他P的本地队列中窃取Goroutine来执行。
  6. 调度器介入:Go运行时的调度器会监控所有的M、P和Goroutines,确保每个M都有工作可做。调度器会在必要时介入,进行M与P的绑定和解绑,以及Goroutines的分配。

通过这种动态绑定和解绑的机制,Go运行时能够有效地利用系统资源,同时保持高并发性能。这种设计允许Go程序在多核处理器上实现高效的并发执行,而不需要程序员进行复杂的并发管理。

对应关系

在Go语言的GMP模型中,M(Machine,操作系统线程)和P(Processor,处理器)之间的对应关系是动态的,而不是一对一的固定关系。这种设计允许Go运行时(runtime)更灵活地调度Goroutines(G),以实现高效的并发执行。下面是M和P之间对应关系的几个关键点:

动态绑定

  • 绑定:一个M在执行Goroutines之前,需要绑定一个P。绑定发生在M准备执行Goroutine时,M会从系统的P池中获取一个可用的P并与之绑定。这个过程是动态的,意味着M可以在不同时间绑定不同的P。
  • 解绑:当M因为某些原因(如系统调用阻塞)不能继续执行Goroutines时,它会释放或解绑当前的P,使得其他的M可以绑定该P并继续执行Goroutines。当M再次准备执行Goroutines时,它需要重新绑定一个P。

多对多关系

  • 多个M可以共享P池:系统中的所有M共享一个P池。这意味着多个M可能在不同时间绑定同一个P(当然,不是同时绑定)。
  • 一个M在任何时刻只能绑定一个P:虽然M可以在其生命周期内绑定多个不同的P,但在任何给定的时刻,一个M只能绑定一个P。
  • 一个P在任何时刻只能被一个M绑定:同样,虽然一个P可以在其生命周期内被多个不同的M绑定,但在任何给定的时刻,一个P只能被一个M绑定。

调度和负载平衡

  • 工作窃取:为了保持所有的M都在工作,Go运行时使用工作窃取算法来平衡不同P的负载。如果一个M通过其绑定的P无法找到可执行的Goroutine,它会尝试从其他P的本地队列中窃取Goroutine来执行。
  • 全局队列和本地队列:P拥有本地队列,用于存储准备执行的Goroutines。当P的本地队列为空时,M可以从全局队列获取Goroutines,或者从其他P的本地队列中窃取Goroutines。

M与G

在Go语言的GMP(Goroutine, Machine, Processor)模型中,M(Machine)与G(Goroutine)的对应关系是多对多的。这意味着一个M可以在其生命周期内执行多个G,而一个G也可以在其生命周期内被多个M执行。这种关系是由Go运行时的调度器动态管理的。以下是M与G之间对应关系的几个关键点:

  1. 多个Goroutines:一个M可以执行多个Goroutines,但在任何给定的时刻,它只能执行一个Goroutine。当一个Goroutine执行完毕、阻塞或者主动让出执行权时,M会从与之绑定的P的本地队列或全局队列中选择另一个Goroutine来执行。
  2. 上下文切换:当一个Goroutine因为I/O操作、系统调用或者其他阻塞操作而不能继续执行时,M会进行上下文切换,挂起当前的Goroutine,并选择另一个Goroutine继续执行。这个过程是由Go运行时的调度器控制的。
  3. 工作窃取:如果M的当前P的本地队列中没有可运行的Goroutine,M可以尝试从其他P的本地队列中“偷取”Goroutine来执行。这是Go运行时为了保持所有M都在工作而采用的一种策略。
  4. Goroutine的状态:Goroutines在它们的生命周期中会有不同的状态,如_runnable_(可运行的)、running(正在运行的)、waiting(等待的)、dead(结束的)。一个Goroutine可能会在多个M之间迁移,这取决于它的状态和系统的调度策略。
  5. Goroutine的调度:Go运行时的调度器使用了一个称为M:N调度的模型,其中M代表操作系统线程,N代表Goroutines。调度器会根据系统的负载和资源情况,动态地将Goroutines分配给M来执行。
  6. 并发执行:尽管一个M在任何时刻只能执行一个Goroutine,但由于Go程序通常会有多个M(操作系统线程)同时运行,因此多个Goroutine可以并发执行。

总结来说,M与G的对应关系是由Go运行时的调度器动态管理的,一个M可以执行多个Goroutines,但在任何给定的时刻只能执行一个Goroutine。这种多对多的关系使得Go能够高效地处理并发任务,同时最大化地利用多核处理器的能力。


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。

Author: mengbin

blog: mengbin

Github: mengbin92

cnblogs: 恋水无意

腾讯云开发者社区:孟斯特