goroutine

在这里插入图片描述
协程的优势在于
1拥有自己的携程栈
2方便保存现场和恢复现场,

GPM模型

在这里插入图片描述

在这里插入图片描述
代码执行过程:
1代码变异后生产可执行文件,拷贝到栈的代码段,包含runtime。main和程序入口
2 操作系统os的初始化
3 执行runtime.main,创建main goroutine
4 runtime.main调用main.main

在这里插入图片描述
代码段包含几个全局变量
1 runtime.g对应协程的数据结构
2 runtime.m对应线程数据结构
3 g0是主协程

在这里插入图片描述
4 g0在主线程栈上分配
5 g0存储m0的指针,m0存储g0的指针,m0一开始执行的是g0
6 allgs记录所有的协程,allm记录所有的线程
在这里插入图片描述

工作队列

7 p对应的数据结构是runtime.p
8 p包含一个本地runq
在这里插入图片描述
p从本地队列分配g给m,如果本地队列为空则从全局队列获取g

在这里插入图片描述
1 全局队列保存在sched中,数据结构是runtime.schedt 代表着调度器
2 sched 中记录空闲的m 空闲的p 和全局的runq
3 若所有的本地队列已满 则将g放到全局队列,
4 m先从本地队列获取g,没有的话再从全局队列获取,全局也没有的话从别的p获取工作量
5程序初始化的时候进行调度器初始化schedinit,由参数GOMAXPROCS这个参数决定创建多少个p,记录在数据段的allp中
6 main goroutine创建之后放到p中,mstart调用schedule函数开始调度,此时队列里只有main goroutine,m0切换到main goroutine,执行入口就是main.runtime,它会创建监控线程,进行包初始化,执行mian.main

在这里插入图片描述

1 如果启动了多个gotoutine,此时有空闲的p,p会启动新的线程m绑定p,然后把goroutine加入到p的本地队列
2 此时可以用channel waitgroup等方法让maingoroutine等待其他g执行完成

G队列-本地/全局

在这里插入图片描述
在这里插入图片描述
go关键字开启goroutine的过程:
1 go关键字在编译阶段会转化为对newproc函数的调用
2 newproc切换到系统栈调用newproc1
3 newproc1找到空闲的g或初始化新的g,并放到本地runq中
4 而放入本地runq的顺序是先放入本地runnext中,先将1放进runnext,再把2放进runnext将1挤走,最后3被分入runnext挤走2,所以执行顺序是先执行runnext中的3,再执行runq中的1-2
5 本地runq记录256个g,runnext记录1个g,共257个g在p的本地队列,第258个g创建后,进入runnext,会将257挤出,但是此时本地runq队列已满,此时本地runq中的一半和257都会被flus到全局runq
6 而实际执行的时候会发现,先执行258,然后本地和全局交替执行,这是因为调度逻辑中p.schedtick每记数到61,就会切换到全局runq中调度执行g
在这里插入图片描述

gouroutine创建-newproc-newproc1

以上面hello函数为例
1main goroutine执行起来会创建hello goroutine函数,创建的任务有newproc来执行

在这里插入图片描述
关键字go等于调用newproc函数创建协程
2 main goroutine加载到main goroutine栈帧,确定bp和sp。
a加载返回地址,
b 调用者栈基
c 局部变量(”name”)
d 返回值
e 参数 fn=&hello() siz=16

在这里插入图片描述
在这里插入图片描述

newproc函数:
1
1 拿到argp和pc
2 通过systemstack切换到G0栈调用了newproc1函数来创建协程

在这里插入图片描述
newproc1的工作:
1getg获取当前的g(这里获取到g0),获取m,通过acquirem()锁定当前m,禁止m被强占
2通过当前m找到p,先从p的本地队列获取空闲的g,如果当前P和sched都没有空闲的g,就创建新的g,添加到全局变量allgs中
3将参数拷贝到获取的g goroutine栈,

a 拷贝参数,startpc执行入口,hello()
b 将goexit+1作为返回地址,(hello goroutine执行完毕时方便goexit执行栈回收)
c sched函数保存现场,调度执行的时候g.sched恢复现场,hello函数开始执行
d 待执行完成后,返回goexit函数,执行资源回收等收尾工作 (相当于在goexit函数中调用了hello gproutine函数)

4 创建goid,调用runqput把g放到运行队列,

a 首先随机把g放到p.runnext, 如果放到runnext则入队原来在runnext的g,
b 尝试把g放到P的"本地运行队列",
c 本地运行队列满了则调用runqputslow把g放到"全局队列" ,runqputslow会把本地运行队列中一半的g放到全局队列

执行

在这里插入图片描述
1 如果当前有空闲的p&&所有的m都在运行&&main以开始执行,则调用wakep()创建一个m来执行G
2 通过channel与main goroutine通信,防止main goroutine过早退出

在这里插入图片描述

出让cpu-gopark-park_m

在这里插入图片描述
1,runtime.gopark先锁定m,再将m上的g从running变成wait,释放m
2 调用mcall保存当前现场,切换到g0栈,调用mcall传入的参数runtime.parkm
3 parkm会根据g0找到当前m,把m.curg.m置为nil,当前m。curg置为nil,即解除g与m的绑定
4 调用schedule寻找下一个待执行的g
5 goready函数将g的状态设置为runnable状态,放到runq中,等待执行

以上总结,

mcall函数:
1 保存当前goroutine的状态(PC/SP)到g->sched中,方便下次调度;
2 切换到m->g0的栈;
3 然后g0的堆栈上调用fn;

gogo函数:
1gogo函数的作用正好相反,用来从gobuf中恢复出协程执行状态并跳转到上一次指令处继续执行

gopark函数做的主要事情分为两点:
1解除当前goroutine的m的绑定关系,将当前goroutine状态机切换为等待状态;
2调用一次schedule()函数,在局部调度器P发起一轮新的调度。

park_m函数主要做的几件事情就是:
1 线程安全更新goroutine的状态,置为_Gwaiting 等待状态;
2 解除goroutine与OS thread的绑定关系;
3 调用schedule()函数,调度器会重新调度选择一个goroutine去运行;

goready函数
1 goready函数相比gopark函数来说简单一些,主要功能就是唤醒某一个goroutine,该协程转换到runnable的状态,
2 将其放入P的local queue,等待调度。

主动让出

在这里插入图片描述

1 协程进入sleep时,状态会从grunning变为gwaiting进入对应timer等待,timer中的回调函数f在timer到达后调用这个函数,使协程恢复到grunnable,并放回runq中
2 每个p都有一个最小堆,堆顶的timer就是下一个要触发回调的,p的timers负责在到达时间后触发回调函数,schedule()在每次调度的时候都会执行checktimer,检查并执行到时的timer
3 监控线程sysmon监听timer的执行,并在没有空闲的m时启动m以保证timer正常执行(增加保障)每10ms执行一次netpoll
4 协程等待队列时,也会将状态从running变为waiting进入channel的读队列或写队列进行等待,方式是队列关闭或可读可写
5协程等待io事件时,也需要让出,状态变为gwaiting,事件执行完毕,恢复到running,等待方式为netpoll主动轮询

最小堆:最小堆

公平调度

在这里插入图片描述
sysmon负责检查p运行每个g的时长
1p.cdhedtick在调度执行一个新的g是自增1,与sysmontick的值对比,若不相等则说明又发生新的调度
2schedwhen记录上一次调度的时间
检测到运行时间过长,如何通知让出呢

在这里插入图片描述
栈增长代码:
1栈帧比较小,sp标识当前栈帧使用到哪里,stardguard0就是空间下界,sp>stackguard,执行栈增长
2检测到将要超出stackguard的部分已经大于stacksmall了,调用morestack栈增长
3如果检测到栈要增长到stackpreempt,调用morestack执行栈增长
在这里插入图片描述

1 morestack函数将会调用runtimr.newstack申请占空间,
2申请前先判断stackguard是否等于stackpreempt,等于就进行调度
3stackpreempt会通知协程让出
4 缺点是只能依靠站增长代码实现,如果是空循环,则会出现死锁
在这里插入图片描述

异步抢占解决了上面的问题
异步抢占----发送信号----接收信号后调用asyncpreempt函数,确认可以抢占以及安全抢占后再调用schedule
在这里插入图片描述
1当一个g切换到g0栈进行系统调用时,m与g强绑定,p暂时用不到,此时m会让出p
2当前g结束系统调用,m会检查oldp是否被占有,如果被占用就重新申请p

schedule如何调度:
在这里插入图片描述

在这里插入图片描述

让出后的m要找到新的g,这个工作由cshedule执行:
1看当前m有没有绑定g,绑定的话当前m就不能执行其他g,此时schedule要阻塞当前m,等到再次调度g时唤醒m
2通过sched.gcwaiting看gc是否在等待执行,是的话调用gcstopm(),执行gc,阻止其他g绑定m
3检查有没有要执行的timer
4 从全局renq中获取g到本地runq
5 通过调用findrunnable()获取待执行的g:

a 检查是否要执行gc
b 从本地runq获取待执行的g
c 从全局runq获取到执行g
d 执行netpoll,恢复io时间就绪的g,将他们放回global runq
e 从其他p中steal一些g—工作量窃取

6 通过5获取到g,先检查获取到的g是否有绑定的m,有的话要还给绑定的m,再次调度g给当前m
7 获取的g没有绑定m,就在当前m上调用excute()执行g:

a 建立当前m与当前g的关联
b 将g从grunnable修改为grunning
c p.schedtick+1
d 调用 gogo(g.sched)函数-恢复g的栈指针,指令指针继续执行,(g创建时newproc就初始化了一个执行现场,即使这个g初次执行,也会恢复执行现场)

抢占式调度

linux下查看goroutine
1 top命令查到pid
在这里插入图片描述
2 对于一个正在运行的程序,可以用dlv attach $pid来追踪

在这里插入图片描述
3 grs命令查看当前所有的协程
在这里插入图片描述

在这里插入图片描述
来分析下grs输出的欣信息:
1 goroutine1和goroutine6是我代码创建的,其余是runtime创建的,
2goroutine对应代码第12行,即mian.mian
3 goroutine6对应第9行,
4 goroutin前面的星号表示当前调试绑定1号协程

在这里插入图片描述

gr 6 切换到6号协程
在这里插入图片描述
再通过bt栈回溯
在这里插入图片描述
是STW导致阻塞,STW要抢占所有的p,使其让出cpu,让gc开始工作

在这里插入图片描述
1,gc记录当前需要等待多少个p

在这里插入图片描述

2 当前p,系统正在调用的p,以及空闲的p,将其设置成pgcstop即可,对于当前正在运行的G的P,将其stackguard设置成stackpreement,标识gc正在等待让出,gcwaiting=1
在这里插入图片描述
具体是如何抢占呢?
1,前面了解了函数在编译阶段编译器会在函数头部插入检测代码,检测函数是否需要栈增长,档期设置成stackpreempt时即表示不会栈增长,而是执行schedule,调度时先检测gcwaiting的标识,若=1,则让出让gc开始工作
在这里插入图片描述
之所以出现上述问题,是因为代码中for只是空循环,也就不会插入栈增长代码,当然不会去检测gcwaiting

发送sigpreempt信号

在这里插入图片描述
抢占实现:
1 函数preemptone实现抢占p,4个步骤
a将g.preempt设置为ture
b 将g.startguard0设置成stackpreempt
c 判断硬件支持
d 判断用户是否允许,以上判断通过则将p.preempt设置成true
2 实际的抢占工作交由preemptM函数完成
3 preempt函数通过调用runtime.signalM发送信号给特定的M
4 发送信号调用了系统调用的SYS_tgkill发送给目标线程
以上实现了抢占的前半部分工作–信号发送,后面的工作有接受到信号的m完成

sighandler处理信号

在这里插入图片描述

1 m接受到sigpreenpt信号,交给sighandler来处理信号
2 sighandler函数确认信号为preempt后调用dosigpreempt函数,它会判断是否对指定的G异步抢占,通过:
a 指定的G与其对应的p,m的preempt字段都为true
b 指定的g处在grunning状态
c 指定的g可以安全的挂起,并对她进行栈扫描,没有打断写屏障
d 有足够的空间注入异步抢占函数
e 当前没有runtime相关的锁,不会引起死锁
3以上确认了要抢占,并且抢占式安全的,调用pushcall注入异步抢占函数
4 异步抢占函数asuncpreempt函数先保存现场,调用runtime.asyncpreempt2函数最终去执行schedule

在这里插入图片描述

所以查看到在go1.14之后,既是for空循环,也调用了asyncpreempt2函数
在这里插入图片描述

dlv常用命令:
在这里插入图片描述
core可以查看具体goroutine的栈帧
在这里插入图片描述

在这里插入图片描述
参考:https://blog.csdn.net/weixin_35655661/article/details/119573864