记得2018年的时候,我还在一个小公司窝着。虽然已经在公司工作了4年多,但是我觉得也是时候走出去了。
于是,我就开始投简历面试 Go 相关的职位了。
我还记得那个是一个算不得天气晴朗的午后,我被面试官有关 go 调度的问题问懵的情景。
面试官:好了,刚才你提到了Go协程调度的GMP模式,那现在有这么一个问题,你看看:如果我现在从协程中发出一个http请求,发往百度,get一些数据。请问,在这个过程中,协程是怎么调度的?
沃茨,你怎么不按套路出牌? 调度的大致流程我都说清楚了啊。
完了,大脑一片空白,你问我发起一个baidu网络请求会发生什么?好像没想过。会切换吗?
我:这个,嗯。。。啊。。。哦。。。,给我5分钟我再思考一下。(5 minutes later)不知道。。。
面试官:好了,回去等消息吧~
唉,看吧,我总是在某一个方面持续弱鸡,不得不事后复盘、学习、总结。
事后,我痛定思痛,希望下次不要再被问倒,这里将总结到的东西分享给大家,希望未来大家过关斩将,一路打通boss!
如雷贯耳的 GMP 模型是怎么来的?
一个程序在执行的时候,我们能get到它的本质,其实就是一个打工者的无限循环。如下:
后来,操作系统支持了线程机制,一条打工人流水线就变成多条打工人流水线了:
打工人需要G0工头帮忙找活干,那活怎么找呢?简单!活都放到池子里,要活干的让他们 G0 工头来取。
每次 G0 来取活都要加锁,防止活被取重了,但是这个效率有点低啊,要不每个循环给分配一个 P?活在被创建的时候可以随机丢到一个 P 池子里。
但是活总有简单和复杂的,不会那么平均的,很快一个 P 就闲的蛋疼了。工头一看,这不是事啊,搞个公共池子,P里没有了就来公共池子里取,嘿嘿,妈妈再也不用担心你没活干了。
不知道大家看出来没有,这里的 G 其实就是那么多的活被拆出来的部分,这么多 G 其实对应的就是逻辑的分布式
很多分布式的逻辑其实可以并行执行,至于这个逻辑在哪个打工人线程上执行,并不需要那么关心。
活怎么分配的逻辑,其实就是协程调度的逻辑。
工头找活干逻辑
这时候,工头 G0 闪亮登场~ 由它来找活。
其实这个G0工头找活干的代码非常简单,就是下面这段:
go
# 找一个可以干的活(gp)
gp := findRunnable()
# 干活
execute(gp)
找活干的逻辑和单线程执行时候的 one loop per thread 非常类似。流程大概是:工头 G0 不停的 loop
有需要执行的定时器活么?
咱们 P 池子里有活干么?如果没有,我老人家(G0)就去公共池子里取一些过来:
再看看有没有七大姑八大姨梢信过来,让帮忙做点事的?这年头,经常有新的连接或者连接上的请求过来,咱们要抓紧了:
也没有吗?那我可要发扬互帮互助的集体主义精神,主动承担灰色地带的任务咯?
张一鸣:哦哟,小伙子,你字节范不错哦!年低给你打M+!
什么?连别人那边的任务都没有?那有点不好意思啊,那么去摸个10ms的鱼不过分吧?
系统自动运行总会出问题的
大家都知道,一个好的系统,不可能自己完全运行流畅无阻,总得需要 G0 工头出面帮解决一些卡点。
现在,第一个问题就来了:G1 兄,你占着这个茅坑拉屎拉了1个小时33分钟了,是不是让一下?G2 兄他快憋不住了,G3 兄差点拉裤裆里了。
G1 兄一听,觉得不好意思,行行,正好我也马上要去问百度一点事情,你就让G2 兄进来吧,瞧瞧,脸都憋红了。下次,早点和 你 G1 叔说嘛~
于是 G0 就领着憋得不行的 G2 进来:
G2 是个好小伙子,就怕麻烦到别人(小日子:是在说我吗?),所以述福了1分钟就主动让贤,退了出来,G0 工头顺势让 G3 上去执行:
这里的协作契机就有很多,所谓协程,就是大家一起协作。怎么协可是很有讲究的。这也是我面试被考倒的地方。于是我特意总结了,哪些情况下,协程G会需要中断手头的活,退出
调度时机
golang的协作式调度和 thread 的抢占式调度不同,需要一些用户事件来作为调度时机。thread 是抢来抢去,协程是协来协去。
协去场景1:
- 主动调度
如果一个 G 觉得自己运行了很长时间,有点不大好意思,就会主动调用 runtime.Gosched。这个函数调用后,会从当前g切换到工头协程g0,这叫主动让贤,也是我们 G2 同学做得事情,鼓掌~ 尧舜禹看了都感动不已。
协去场景2:
- 被动调度
大部分情况下,G 做了一些事情,需要等待一段时间,这时候就会触发被动调度。G被动被协走的情况有很多,如
- sleep(写文章写得头晕,去睡一下不过分吧? 这里得场子就交给工头兄帮忙看一下了)
- 加锁等,atomic/mutex(一时半会也锁不上,嘿嘿,先happy去,然后等通知)
- channel 阻塞(哎,怎么就我在这等活,惨,趁着活还没来,先眯一会)
- 网络IO(去问百度一个事情,百度返回之前我不能干等着吧?先干点别得)
- 系统调用(我这活一时半会干不完,要等上面来信,先干点别得)
- 执行GC(门前雪得扫一扫了,不能天天吃喝玩乐呀)
但总有一些 G 不识抬举的,都执行半天了也不知道下来,不自觉!
此时呢,工头就得想点办法,强制打断这家伙。比如:
- 每次切换函数得时候,判断一下你丫得是不是占了太长时间得茅坑了?
这时候,如果有长期摸鱼得,就被抓个正着。
也有一些 G 想利用漏洞,唉?我就不切换函数,就死命跑循环,来打我啊?
为此 go1.14 引入了基于信号的强制抢占策略。主要就是在信号处理函数中,做异步抢占。
好了,漏洞都堵上了,结束。
现在大家都其乐融融,一起过上了快乐得日子。