Golang深入理解GPM模型

时间:2024-10-11 07:26:09

简介

多进程、多线程已经提高了系统的并发能力,在高并发场景下,如果一个线程阻塞cpu,那就需要切换其他线程中去执行,为每个任务都创建一个线程成本比较高,因此又衍生出协程。我们知道一个线程分为“内核态“线程和”用户态“线程,goroutine是Go语言实现的用户态线程, 协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。

在这里插入图片描述

思考为什么协程和线程是M:N映射关系?
M个协程可以在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速,但是一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,无并发能力,因此需要N个线程

线程成本高主要表现在以下两个方面:

  1. 调度的高消耗CPU;操作系统线程的创建和切换都需要进入内核,都会占用很长时间,导致CPU有很大的一部分时间都被用来进行进程调度了
  2. 高内存占用;内核在创建操作系统线程时默认会为其分配一个较大的栈内存(进程虚拟内存会占用4GB[32位操作系统], 而线程也要大约4MB)

而相对的,用户态的goroutine则轻量得多:

  1. goroutine是用户态线程,创建和切换都在用户代码中完成而无需进入操作系统内核
  2. goroutine启动时默认栈大小只有2k,可自动调整容量

2.调度模型组成

1.被遗弃的GM模型

在这里插入图片描述
在12年的go1.1版本之前用的都是GM模型, G为Goroutine协程,M为Machine内核级线程, M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。

1.创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
2.很差的局部性。比如当G中包含创建新协程的时候,M0创建了G1,因为将G1放入全局队列,,需要把G1交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M

模型组成

在这里插入图片描述
调度器主要由以下几部分组成:
schedt结构体:go调度器主体结构,保存调度器自身的状态信息,另外还有goroutine全局队列、空闲的工作线程M、空闲的p结构体对象等其他信息

// 重要的全局变量
var (
	allgs      []*g   // 存储所有g的数组
	allm       *m     // 存储所有m的链表,通过 链向下一个m
	allp       []*p   // len(allp) == gomaxprocs
	gomaxprocs int32  // 最大可创建p的个数,默认为cpu个数,可通过环境变量设置
	ncpu       int32  // cpu个数
	m0   m           // 代表进程的主线程
    g0   g           // m0的g0,也就是m0.g0 = &g0
	sched      schedt
)
type schedt struct {
   
   // 空闲状态的m
   midle muintptr // idle m's waiting for work
   // 空闲状态的m个数
   nmidle int32 // number of idle m's waiting for work
   // m允许的最大个数
   maxmcount int32 // maximum number of m's allowed (or die)
   nmsys     int32 // number of system m's not counted for deadlock
   nmfreed   int64 // cumulative number of freed m's
   // 系统中goroutine的数目,会自动更新
   ngsys uint32 // number of system goroutines; updated atomically
   // 空闲的p列表
   pidle puintptr // idle p's
   // 有多少个状态为空闲的p
   npidle uint32
   // 有多少个m自旋
   nmspinning uint32 // m处于自旋状态时,是当前m没有g可运行,正在寻找可运行的g
   // Global runnable queue.
   // 全局的可运行的g队列
   runqhead guintptr
   runqtail guintptr
   // 全局队列的大小
   runqsize int32
   // freem is the list of m's waiting to be freed when their
   //  is set. Linked through .
   freem *m

}

g的结构体: 它保存了goroutine的所有信息, 如保存调度信息

type g struct {
   
   // 简单数据结构,lo 和 hi 成员描述了栈的下界和上界内存地址
   stack       stack   // offset known to runtime/cgo
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr // offset known to liblink
   // 当前的m
   m *m // current m; offset known to arm liblink
   // goroutine切换时,用于保存g的上下文,包括栈顶、栈低、pc等寄存器
   sched     gobuf 
   // 唯一的goroutine的ID
   goid int64
   // 标记是否可抢占
   preempt        bool  
}

m结构体: Machine内核级线程, 每个工作线程都有唯一一个m结构体的实例对象与之对应,线程通过 ThreadLocal 存储各自的m对象;m结构体对象主要记录着工作线程的诸如栈的起止位置、当前正在执行的goroutine以及是否空闲等等状态信息之外,还通过指针维持着与p结构体的实例对象之间的绑定关系。

type m struct {
   
   // 用来执行调度指令的 goroutine
   g0      *g     // goroutine with scheduling stack
   morebuf gobuf  // gobuf arg to morestack
   // thread-local storage
   tls      [6]uintptr // thread-local storage (for x86 extern register)
   mstartfn func()
   // 当前运行的goroutine
   curg      *g       // current running goroutine
   // 关联p和执行的go代码
   p     puintptr // attached p for executing go code (nil if not executing go code)
   nextp puintptr
   id    int64
   // 用于标识睡眠还是唤醒
   park        note
   // 是否自旋,自旋就表示M正在找G来运行
   spinning bool // m is out of work and is actively looking for work
   // m是否被阻塞
   blocked bool // m is blocked on a note
   // 用于链接allm
   alllink   *m // on allm
   schedlink muintptr 
   //...
}

p结构体: P全称为Processor,所有的P都在程序启动时创建,每一个m都会与一个p结构体的实例对象关联在一起, p主要维护一份局部goroutine运行队列

type p struct {
   
   // id也是allp的数组下标
   id     int32
   status uint32 // one of pidle/prunning/...
   // 单向链表,指向下一个P的地址
   link puintptr
   // 每调度一次加1
   schedtick uint32 // incremented on every scheduler call
   // 回链到关联的m
   m       muintptr // back-link to associated m (nil if idle)

   // Queue of runnable goroutines. Accessed without lock.
   // 可运行的goroutine的队列
   runqhead uint32
   runqtail uint32
   runq     [256]guintptr
   // 下一个运行的g,优先级最高
   runnext guintptr  
}

3.浅析调度策略

在这里插入图片描述

  1. 我们通过 go func()来创建一个goroutine
  2. 有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中
  3. G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会向其他的MP组合偷取一个可执行的G来执行
  4. 一个M调度G执行的过程是一个循环机制,如下伪代码所示
// 程序启动时的初始化代码
......
for i := 0; i < N; i++ {
    // 创建N个操作系统线程执行schedule函数
     create_os_thread(schedule)