携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
Golang中是没有线程概念的,大家在开发过程中通常把Golang中的“协程”称作“线程”,从后文可以知道,这不能算错,但肯定算不上非常准确。在这个系列中,我就想分别从宏观和微观的层面,去深入剖析一下GMP模型的具体实现原理,以及分享一下我在了解GMP模型后的一些收获。
Golang中协程实现所依赖的核心机制被称作GMP模型。其中G代表goroutine协程,M代表内核态线程(thread),P代表处理器(processor,不等同于CPU核心)。
将整个GMP模型按照从底层到高层的维度进行划分,可以分为内核态部分和运行在用户态的调度器两部分。内核态部分包括底层硬件(这里主要指的是计算用到的CPU核心)、OS级别的调度器、内核级线程(也就是上文中的M,thread)。M是真正运行G的实体,OS调度器负责把内核线程分配到底层硬件的CPU核心上去实际执行。其实说到“运行”,可能还是比较抽象,这里倒不如换一个词——“调用”。对多线程编程有一点了解的话,就可以知道,线程的作用简单来说其实就可以理解为执行一个函数,然后线程就是和进程类似的一个资源组织单元,只不过它拥有更少的资源,所以也就更轻量,本篇文章不会在进程和线程的区别方面做过多展开,仅仅是想说明这里看起来比较抽象的“运行”其实就是调用函数的意思,而协程其实本质就是个函数。“协程”这个名字中的“协”就可以体现出来它的特点:侧重于“协作”、“协同”,所以同时存在多个协程时就会牵扯到调度的问题,这就涉及到GMP模型中调度器的实现。调度器的功能是把可运行的G分配给可工作的M。调度器的组成包括P、P维护的本地G队列、全局G队列。P包含了运行G的资源,如果M想运行G,则必须先获取P。全局G队列维护了当前等待运行的G(waiting list),P的本地G队列与全局G队列作用类似,不过每个本地G队列最多存256个G,如果新建G时发现所在P下面的本地队列满了,则将队列中的前一半G打乱顺序和新建的G一起插入到全局队列里面。我理解这里的乱序可能是为了防止饥饿现象的发生,在某种意义上提高鲁棒性?暂时没有找到说到这块原因的资料,后面打算在源码中找一下这一段再研究一下。在一个go二进制程序启动时会一次性创建GOMAXPROCS个P(这是个环境变量),维护在数组里。M和P的数量没有绝对的关系,因为如果一个M发生了阻塞,那么P就会去尝试寻找空闲(也就是不可工作)的M,如果没有空闲的,则会创建新的M,感觉也算是一种贪心的原则吧。