Golang--协程调度

协程

bilibili

Goroutine调度模型

早期调度模型

  • G:goroutine,Go协程,对应数据结构:runtime.g
  • M:machine,工作线程,对应数据结构:runtime.m

在程序地址空间的数据段中有重要的全局变量:

  • g0:主协程对应的G。不同于其他的协程,g0的协程栈在主线程栈上进行分配。
  • m0:主线程对应的M。
  • allg:用于记录所有的G。
  • allm:用于记录所有的M。

g0和m0中都分别记录了对方的地址,m0最开始的执行的协程就是g0。

Golang的早期调度器中只有G和M,所有的G被维护在一个全局队列中,所有的M互斥地从全局队列中拿取G执行。但是多个M访问全局队列时频繁的加锁和解锁,会导致M的等待影响程序并发性能。

若G中进行了系统调用,则OS会将对应的M阻塞,则能够从全局队列中拿取G的M就少了,代表执行能力变弱了。

若全局队列中大部分G都会进行系统调用,则就会让大部分M进入阻塞状态,全局队列产生堆积。

对于该问题,需要对线程池中的M数量做把控,太多了也会会由于多个线程抢占CPU,反而导致执行能力下降。

GMP模型

在GM的基础上,又引入了P。

  • P:processor,包含运行Go代码的必要资源,也有调度goroutine的能力,对应数据结构:runtime.g

每一个P中维护了一个自己的本地队列。

代码段中添加有全局变量:

  • sched:调度器,其中记录了所有空闲的M和P,以及全局队列等与调度相关的内容。
  • allp:保存了所有的P

在调度器初始化时,会根据GOMAXPROCS该环境变量决定创建多少个P保存于allp中;并将第一个P(allp[0])与m0进行关联。

将一个P关联到一个M,该M就能从P的本地队列中获取G,而不再只能从全局队列中去获取。

若P的本地队列满了,等待执行的G就会被放入全局队列。

M会优先从P的本地队列拿取G执行,若P的本地队列空了,再到全局队列中拿取G;若全局队列也空了,M会从别的M关联的P中偷取一定的G进行分担,一般一次偷取一半。

GMP执行大致过程

  1. schedinit:调度器初始化
  1. new main goroutine:调用newproc函数创建main goroutine。

newproc的参数为由用户指定调用的函数f(即goroutine运行入口)以及需要传入f的参数。

newproc会为goroutine构造一个栈帧,方便goroutine结束后调用goexit函数来进行协程的回收处理,决定该goroutine是放回空闲G队列备用还是直接销毁。

将main goroutine加入到allp[0]的本地队列中。

  1. mstart:开启调度循环

mstart是所有工作线程的入口,主要通过调用schedule函数来执行调度循环。

对于一个活跃的M,要么是正在执行某个G,要么是正在执行调度程序获取某个G。

  1. runtime.main:mian goroutine的执行入口,其会创建监控线程,初始化包等操作。

其中包括调用main.main开始执行用户编写的语句。

main.main返回之后,runtime.main会调用exit函数结束进程。

  1. 假设我们执行的是以下代码:
go 复制代码
package main

import (
	"fmt"
	"time"
)

func hello() {
	fmt.Println("Hello World")
}

func main() {
	go hello()
	time.Sleep(1*time.Second)
}

go hello()会调用newproc创建一个goroutine,我们称为hello goroutine

  • **GOMACPROCS**为1,则hello goroutine会进入allp[0]的本地队列。

time.Sleep会让main goroutine让到timer中进行等待。

m0调用schedule函数进行调度,让hello goroutine得以运行。

当main goroutine的等待时间结束,会被放入allp[0]的本地队列中。

最后main goroutine结束m0调用exit结束进程。

  • **GOMACPROCS**>1,意味着不止有一个P,则可能会启动新的线程来关联空闲的P。

之后再将hello goroutine放入到空闲的这个P的本地队列中。


GMP调度策略

队列轮转

P会将其本地队列中的G周期性地调度到M中执行,执行一段时间,将上下文保存,放入队列尾部,再从队列拿取一个G调度。

每个P也会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中的G主要来自于从系统调用中恢复的G。为了保证全局队列中的G不会被饿死,故P会周期性查看全局队列。

系统调用

当某个M在执行的G中发生了系统调用,该M会释放掉其关联的P,由别的空闲的M来获取P继续执行P的本地队列中剩下的G。

而之前的G发生系统调用结束后,根据执行它的M是否能获取到P,对该G进行不同的处理:

  1. 有空闲的P,获取一个P,继续执行G
  2. 没有空闲的P,将G放入全局队列,等待被其他的P调度。M进入线程池休眠。
相关推荐
饕餮争锋2 小时前
Supabase使用演示
后端·开源
格林威2 小时前
工业相机图像高速存储(C++版):直接IO存储方法,附海康相机实战代码!
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
代码雕刻家2 小时前
3.1.课设实验-Java核心技术-检索简历
java·开发语言
aZhe的全栈知识分享2 小时前
OpenClaw(龙虾)太难装?这份保姆级教程让你 3 分钟搞定
前端·人工智能·后端
小此方2 小时前
Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析
开发语言·c++
共享家95272 小时前
Java 入门(IDEA 高效调试 与 数组)
java·开发语言·intellij-idea
火山上的企鹅2 小时前
Qt/QGroundControl 实战:接入 Skydroid(云卓) G20 遥控器 Android SDK 并实时显示摇杆与信号质量
android·开发语言·qt·qgroundcontrol·云卓sdk
小小的木头人2 小时前
Ubuntu 20版本中破坏: libgcc-s1冲突
linux·运维·ubuntu
曾阿伦2 小时前
Python项目管理从Poetry迁移到uv:极速体验与实操指南
开发语言·python·uv