Golang GMP模型

2023-08-31 10:59:55 浏览数 (2)

Background

早期操作系统是单进程的,只能顺序执行进程,如果进程需要IO,必须要等IO结束才能继续运行,造成了严重的CPU资源的浪费。

为了提升CPU利用率,出现了多进程操作系统,当一个进程被阻塞,可以切换到其他进程运行,大大减少了CPU资源的浪费。

但是进程的切换开销较大,为了更好地实现并发,出现了线程。一个进程可以有很多个线程,他们共享进程的地址空间,切换带来的开销也要比进程切换的开销小。

但是线程切换也要涉及用户态和内核态之间的切换,不够轻量级,于是将线程一分为二,分别是用户线程内核线程。用户线程负责业务上的处理,内核线程负责操作系统层面的处理。

用户线程就称为协程,内核线程还称为线程,协程的调度需要通过协程调度器来实现,协程调度器为内核线程绑定多个协程。

GMP就是Go的goroutine调度模型。

Goroutine内存占用小,一般是几KB,因此可以大量创建;并且可以灵活调度,因为它的切换成本低。

GMP

G. M. P.

  • G代表goroutine。G中存放并发执行的代码入口地址、上下文、运行环境(关联的P和M)、运行栈等执行相关的信息。
  • M是一个内核线程。是操作系统层面调度和执行的实体。
  • P是处理器,是一个抽象的概念,用于处理G,代表M和G所需要的资源。P是一个管理的数据结构,P主要是降低M对G的复杂性,增加一个间接的控制层数据结构。

P持有G的队列,P可以隔离调度,解除P和M的绑定就解除了M对一串G的调用。G并不是执行体,而是存放并发执行体的元信息,包括并发执行的入口函数、堆栈、上下文等信息。为了减少对象的分配和回收,G对象是可以服用,只需要将相关元信息初始化为新值即可。 P的数目默认是CPU核心的数量,M和P的数目差不多,但运行时会根据当前的状态动态地创建M,M有上限值10000;G与P是M:N的关系,M可以成千上万,远远大于N。

线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到线程上。

GMP结构

图引自Golang深入理解GPM模型

  • 全局队列(Global Queue):存放等待运行的G
  • P的本地队列:存放等待运行的G,但是存储的G数量有限,不超过256个。新建G时,G优先加入到P的本地队列,如果队列满,则会把本地队列中的一半G移动到全局队列。
  • P列表:所有的P都在程序启动时创建,并保存在数组中。P的数量可以通过环境变量$GOMAXPROCS来设置;或者在程序中通过runtime.GOMAXPROCS()来设置。
  • M:线程想要运行任务就得获得P,从P的本地队列中获取G,P队列为空时,M也会尝试从全局队列拿一批G放到本地队列,或者从其他P的本地队列拿一半放到自己的P的本地队列。
  • M列表,当前操作系统分配到当前Go程序的内核线程数,Go语言限定M的最大量是10000
  • M运行G,G执行之后,M会从P获取下一个G,不断重复下去。M与P是1:1的关系。

调度策略

M和P构成一个运行时环境

  • P优先从本地队列中获取goroutine执行
  • 之后从全局队列中获取goroutine执行
  • 再之后去其他的P的本地队列中steal goroutine执行
  • 并不完全按照以上顺序来,会在执行完61个本地goroutine之后,去全局队列尝试拿goroutine执行,避免全局队列中的goroutine饿死
  • 当一个线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行,自己只服务当前这个阻塞了的G。本质是不让CPU闲着,阻塞就切换CPU。当阻塞了的G恢复执行后,在当前线程上执行完之后,还想继续执行,会加入到其他的P队列中,而当前线程会睡眠/销毁。
  • 抢占,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死。

当执行go func()时发生了什么?

图引自Golang深入理解GPM模型

  1. 创建一个Goroutine
  2. 放入执行go func()的线程对应的P的本地队列中 2.1 如果本地队列已满,则放入全局队列中
  3. M获取G
  4. 调度
  5. 执行,去运行G中的func()函数 5.1 如果执行时,G.func()发生阻塞 5.2 创建一个M或从休眠队列取一个M 5.3 接管当时正在阻塞G的P
  6. 时间片超时,将G重新放回到队列尾部

0 人点赞