【概念】
G(Goroutine):即Go协程,每个go关键字都会创建一个协程。
M(Machine):工作线程,在Go中称为Machine,数量对应真实的CPU数(真正干活的对象)。
P(Processor):处理器(Go中定义的一个概念,非CPU),包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS() 来设置,默认为逻辑CPU数量。
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。
【图示】
在任一时刻,一个P可能在本地包含多个G,同时一个P在任一时刻只能绑定一个M。
不过上边的只是一个GMP的概念模型,真正的GMP模型机制要比上边的更复杂。
● 协程的生命周期和状态
【说明】
和Java中线程被分成的各种状态类似,golang的调度器将协程也分为多种状态
【图示】
● 特殊协程g0与协程切换
一般的协程有main协程和子协程,main协程在整个程序中只有一个,深入go语言运行时会发现,每个线程中都有一个特殊的协程g0。
【概念】
协程g0运行在操作系统线程栈上,其作用主要是执行协程调度的一系列运行时代码,而一般的协程无差别地用于执行用户代码。
在用户协程退出或者抢占时,意味着需要重新执行协程调度,这是需要从用户协程g切换到协程g0,每个线程的内部都在完成这样的切换与调度循环。golang运行时将线程的用户态内核态切换转移到了线程中的g与g0的切换,这种切换叫做协程的上下文切换。
● 调度循环
【概念】
调度循环指从调度协程g0开始,找到接下来将要运行的协程g,再从协程g切换到协程g0开始新一轮调度的过程。与上下文切换不同的是上下文切换是一个线程执行不同状态的切换,调度循环关注的是不同协程切换的流程。
● 线程本地存储与线程绑定
线程本地存储是一种计算机编程方法,线程本地存储中的变量只对当前线程可见,这种类型的变量可以看作是线程私有的,比如Java里的threadlocal,一般操作系统用FS/GS段寄存器存储线程本地变量。
go运行时使用线程本地存储将操作系统内核态的线程和运行时代表线程的m结构体绑定在一起,所以线程本地存储的结构体m会存储当前线程正在运行的g的地址。
因此在任意一个线程内部通过线程本地存储都可以获取线程上的协程g,特殊协程g0,逻辑处理器p以及结构体m。
● 调度时机
即什么时候会发生调度?
调度时机分为主动、被动和抢占调度。
主动调度的原理比较简单,P 会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度。除此之外,P还会周期性的查看全局队列是否有G等待调度到M中执行。
被动调度:指协程在休眠,channel通道阻塞,网络IO阻塞,执行垃圾回收而暂停时,被动让渡自己执行权利的过程。和主动调度不同的是,被动调度不会将G放入全局运行队列,因为当前G的状态不是_Grunnable而是_Gwaiting,所以被动调度需要一个额外的唤醒机制,先将状态转换成_Grunnable。
执行时间过长的抢占调度:为了让每个协程都有执行机会,最大化利用CPU系统,go语言在初始化时会启动一个特殊的线程来执行系统的监控任务。系统健康会在一个独立的M上运行,不用绑定逻辑处理器P,系统每10ms会检测是否有准备就绪的网络协程,并放置到全局队列中。系统监控服务会判断当前协程执行时间是否过长,对于执行时间过长的协程
● 调度策略
总体来说调度策略如下,如果局部队列为空,则尝试从全局队列读取需要执行的G,如果全局队列也没有找到要执行的G,调度器会寻找当前是否有以及准备好运行的网络协程,go语言只能够的网络模型也是对不同平台上IO多路复用技术(epoll/kqueue/iocp)的封装,如果都没有则会尝试从其他P中窃取可用的协程(所以P都存储在一个全局队列中)。
(调度的核心策略在schedule函数中,schedule函数首先会检测程序是否处于垃圾回收阶段,如果是,则检测是否需要执行后台标记协程。程序不可能同时执行所有协程,等待被调度的协程会存储在运行队列中go语言调度器将运行队列中。
go语言调度器将运行队列分为局部运行队列和全局运行队列。局部运行队列是每个P结构特有的长度为256的数组,该数组模拟了一个循环队列,每次将G放入本地队列时,都从循环队列的尾部插入,获取时从循环队列的头部获取。
一般的思路是先查找每个P的局部运行队列,当获取不到局部队列时,再从全局队列中获取。
但是这种方法有一个问题,如果只是循环往复地执行局部运行队列中的G,那么全局队列中的G可能完全不会执行。为了避免这种现象,go语言调度器使用了一种策略,P中每执行61次调度,就需要优先从全局队列中获取一个G到当前P中,并执行下一个G。)