GMP调度模型解析

GMP是Go语言运行时的一种调度模型,用于管理Goroutines、内存和处理器资源。

一、G、M、P分别指的什么?

1.1 G(goroutine):Goroutine是Go语言中的轻量级现成。

  • goroutine的新建, 休眠, 恢复, 停止都受到go运行时的管理
  • goroutine执行异步操作时会进入休眠状态, 待操作完成后再恢复, 无需占用系统线程
  • goroutine新建或恢复时会添加到运行队列, 等待M取出并运行

1.2 M(machine):代表操作系统系统的线程。每个M是一个实际的操作系统线程

  • M会从P(本地队列)中取出G,然后运行G,如果G运行完毕或进入休眠状态,则从运行队列中取出下一个G运行,周而复始。
  • 有时G需要调用一些无法避免阻塞的原生代码,这时M会继续保持阻塞状态 等待G的执行,但是P会被释放。P被释放后被其他空闲M继续调用
  • go代码, 即goroutine, M运行go代码需要一个P
  • 原生代码, 例如阻塞的syscall, M运行原生代码不需要P

1.3 P(process):代表处理器。是Go运行时系统重的一个逻辑处理单元,包含了任务队列和与执行goroutine相关的资源。

  • P的数量通常由GOMAXPROCS指定,默认值为运行环境中的CPU核数。
  • 如果P的数量等于1,代表当前最多只能有一个线程(M)执行go代码
  • 如果P的数量等于2,代表当前最多只能有两个线程(M)执行go代码

二、调度模型

2.1 Goroutine创建与分配:

  • 当你创建一个新的Goroutine时(例如go task()),这个Goroutine被放置在某个P处理器的任务队列中。
  • 如果所有P的任务队列都满了,新的Goroutine会被放置到全局队列中。

2.2 M线程获取P处理器:

  • 每个M线程都会获取一个P处理器,并开始执行这个P处理器任务队列中的Goroutine。
  • 一个M线程一次只能与一个P处理器绑定,并执行这个P中的一个Goroutine。

2.3 执行与切换:

  • M线程运行P处理器的任务队列中的Goroutine,直到这个Goroutine完成或被阻塞(例如等待I/O)。
  • 如果一个Goroutine被阻塞,M线程会将其挂起,并尝试从P处理器的任务队列中获取下一个可运行的Goroutine。
  • P处理器的任务队列为空时,M线程会尝试从全局队列中获取任务或从其他P处理器的任务队列中偷取任务。

2.4 任务抢占:

  • Go调度器会定期检查是否需要切换Goroutine,以确保所有Goroutine都有机会运行。
  • 如果一个Goroutine运行时间过长,它可能会被抢占,以便其他Goroutine有机会运行。

2.5 总结:

  • Goroutine是轻量级的协程,被放置在P处理器中。

  • M是操作系统的线程,一个M线程一次只能执行一个P处理器中的一个Goroutine。

  • 调度器负责在多个M线程和P处理器之间公平地分配Goroutine,以实现高效的并发执行。

三、数据结构

3.1 G的状态

  • 空闲中:表示G刚创建,仍未初始化
  • 待运行:G刚在运行队列中,等待M取出运行
  • 运行中:M正在运行G,此时M会拥有一个P
  • 系统调用中:M正在运行这个G发起的系统调用,这时候M并不拥有P
  • 等待中:G在等待某些条件完成,这时G不在运行也不在运行队列中
  • 已中止:G未被使用,可能已执行完毕
  • 栈复制中:G正在获取一个新的栈空间并把原来的内容复制过去

3.2 M的状态

  • 自旋中:M正在从运行队列中获取G,这时M会拥有一个P

  • 执行go代码中: M正在执行go代码, 这时候M会拥有一个P

  • 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P

  • 休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P

3.3 P的状态

  • 空闲中:当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
  • 运行中:当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
  • 系统调用中:当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
  • GC停止中:当gc停止了整个世界(STW)时, P会变为此状态
  • 已中止:当P的数量在运行时改变, 且数量减少时多余的P会变为此状态

3.4 本地运行队列

  • go中有多个运行队列可以保持待运行的G。分别是各个P中的本地运行队列和全局运行队列。

  • 入队待运行的G时会优先加到当前P的本地运行队列, M获取待运行的G时也会优先从拥有的P的本地运行队列获取。

  • 本地运行队列入队和出队不需要使用线程锁。

  • 本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列。

  • 本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成。

3.5 全局运行队列

  • 全局运行队列保存在全局变量sched中, 全局运行队列入队和出队需要使用线程锁。

  • 全局运行队列的数据结构是链表, 由两个指针(head, tail)组成。

3.6 空闲M链表

当M发现无待运行的G时会进入休眠,并添加到空闲M链表中,空闲M链表保存在全局变量sched

进入休眠的M会等待一个信号量(m.park),唤醒休眠的M会使用这个信号量。

go需要保证有足够的M运行G,实现机制是:

  • 入队待运行的G后, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M3
  • 当M离开自旋状态并准备运行出队的G时, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
  • 当M离开自旋状态并准备休眠时, 会在离开自旋状态后再次检查所有运行队列, 如果有待运行的G则重新进入自旋状态

"入队待运行的G"和"M离开自旋状态"会同时进行, go会使用这样的检查顺序:

入队待运行的G => 内存屏障 => 检查当前自旋的M数量 => 唤醒或者新建一个M
减少当前自旋的M数量 => 内存屏障 => 检查所有运行队列是否有待运行的G => 休眠

3.7 空闲P链表

当P的本地运行队列中的所有G都运行完毕, 又不能从其他地方拿到G时,拥有P的M会释放P并进入休眠状态, 释放的P会变为空闲状态并加到空闲P链表中, 空闲P链表保存在全局变量sched

下次待运行的G入队时如果发现有空闲的P, 但是又没有自旋中的M时会唤醒或者新建一个M, M会拥有这个P, P会重新变为运行中的状态。

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值