微信搜索【脑补炸鱼】关注这条炸肝炸鱼。本文GitHubgithub.com/eddycjy/blog已收录,附有我的系列文章、资料和开源Go书籍。大家好,我是炸鱼。金三银四最近,正是面试季。在我的Go读者交流群里,很多小伙伴都讨论了自己在面试中遇到的一些Go面试题。今天的主角是围棋面试中万能题GMP模型的延伸题(question),就是“GMP模型,为什么会有P?”想来想去,这道面试题的实质是问:“GMP模型,为什么不直接绑定G和M,多创建一个P,好麻烦,目的是什么,要解决什么问题?”本文将带大家一探,GM、GMP模型变化的原因是什么Go1.1之前的GM模型Go的调度模型其实就是GM模型,也就是没有P,今天就带大家一起回顾过去的设计。解密Go1.0的源码我们理解一个东西的方法之一就是看源码,用建宇看一下Go1.0.1的调度器源码的核心关键步骤:staticvoidschedule(G*gp){...计划锁();if(gp!=nil){...switch(gp->status){caseGrunnable:caseGdead://不应该运行!runtimethrow("gp->statusinsched");caseGrunning:gp->status=Grunnable;输出(gp);休息;}gp=nextgandunlock();gp->readyonstop=0;gp->status=Grunning;m->curg=gp;gp->m=m;...runtime·gogo(&gp->sched,0);}调用schedlock方法获取全局锁。成功获取全局锁后,将当前Goroutine状态从Running(被调度)状态变为Runnable(可被调度)状态。调用gput方法保存当前Goroutine的运行状态等信息,以供后续使用;调用nextgandunlock方法寻找下一个可运行的Goroutine,并为其他调度器释放全局锁。获取下一个Goroutine运行后,将其运行状态更改为Running。调用runtime·gogo方法运行刚刚获取到的下一个待执行的Goroutine。关于GM模型的思考通过分析Go1.0.1中调度器的源码,我们可以发现一个比较有意思的地方。那就是调度器本身(schedule方法),在正常流程下,它不会返回,也就是不会结束主流程。他将继续运行调度进程。GoroutineA完成后,会开始寻找GoroutineB。当找到B时,它会将完成的A的调度权交给B,这样GoroutineB就会开始被调度,也就是运行。当然,也有被屏蔽(Blocked)的G。假设G正在做一些系统和网络调用,它会导致G停顿。这时,M(系统线程)会被放回内核队列中,等待新一轮的唤醒。GM模型的缺点从表面上看,GM模型似乎坚不可摧且完美无缺。但是为什么要改呢?2012年,DmitryVyukov发表了一篇文章《Scalable Go Scheduler Design Doc》,至今仍是Go调度器各大研究文章的主要对象。在文章中,他描述了总体原因和考虑。以下内容将参考本文。当前(指Go1.0的GM模型)Goroutine调度器限制了Go编写的并发程序的可扩展性,尤其是高吞吐量的服务器和并行计算程序。实现存在以下问题:全局互斥体(Sched.Lock)单一,状态管理集中:互斥体需要保护所有goroutine相关操作(创建、完成、重排等),导致锁竞争严重。Goroutine转移问题:goroutine(G)交接(G.nextg):Workerthreads(M's)会经常交接可运行的goroutines。以上可能会导致延迟增加和额外开销。每个M都必须能够执行任意一个可运行的G,尤其是刚刚创建了一个G的M。每个M都需要做一个内存缓存(M.mcache):会导致资源消耗过大(每个mcache可以吸收2M内存缓存和其他缓存),数据局部性差。频繁的线程阻塞/解除阻塞:在系统调用存在的情况下,线程经常被阻塞和解除阻塞。这增加了很多额外的性能开销。GMP模型为了解决GM模型的上述问题,在Go1.1中,DmitryVyukov在GM模型的基础上增加了P(Processor)组件。并实现WorkStealing算法来解决一些新问题。GMP模型在之前的文章《Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?》中已经讲解过了。觉得不错的朋友可以关注下,这里不再赘述。加P后会带来什么变化?让我们更明确一点。每个P都有自己的本地队列,大大减少了对全局队列的直接依赖,结果就是锁竞争的减少。GM模型的大部分性能开销是锁竞争。在各个P的相对平衡上,GMP模型中也实现了WorkStealing算法。如果P的本地队列为空,它会从全局队列或其他P的本地队列中窃取可运行的G来运行,减少空闲,提高资源利用率。这时候有朋友会疑惑为什么会有P,如果要实现本地队列和WorkStealing算法,为什么不直接加在M里面呢,M也可以实现类似的组件。为什么另一个P组件?结合M(系统线程)的定位,如果这样做的话,存在以下问题:一般来说,M的数量会比P多。像Go中,M的默认数量是10000,默认P的数量是CPU核心的数量。另外,由于M的属性,即如果有系统阻塞调用阻塞M而不够用,M会不断增加。如果M不断增加,如果本地队列挂载在M上,意味着本地队列也会相应增加。这显然是不合理的,因为本地队列的管理会变得复杂,WorkStealing的性能也会大大降低。M被系统调用阻塞后,我们期望将他未执行的任务分配给其他继续运行的任务,而不是一阻塞就全部停止。因此,使用M是不合理的,那么引入一个新的组件P,将本地队列与P关联起来,就可以很好的解决这个问题。总结今天的文章结合了整个Go语言调度器的一些历史情况、原因分析和解决方案说明。“GMP模型,为什么有个P”这个问题就像一个系统设计的理解,因为现在很多人为了应对面试,或者通过方便面,都会强行背GMP模型。而了解其背后的真正原因,才是我们要学会了解的。知其所以然,知其所以然,方能破局。如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中,微信搜索【脑补炸鱼】即可阅读,回复【000】一线大厂面试算法方案和资料我都准备好了;本文已收录在GitHubgithub.com/eddycjy/blog,欢迎Star提醒。
