Go GMP调度模型

Go 调度器与GMP模型

前奏

关于计算机的简单描述

----------- |-----------------用户应用程序(微信、QQ、浏览器)
| 计算机 |-----------操作系统(linux、windows、mac os)
----------- |-----硬件(CPU、内存、显卡、鼠标、键盘、显示器)

由以上文字可以看出,计算机由一堆硬件构成,操作系统运行于硬件的上层,狭义上来考虑,操作系统是为了协调硬件、管理硬件、提供服务。用户应用程序运行于操作系统之上。那么没有操作系统,我们想运行自己的软件能否实现呢?答案是可以的,裸机程序需要自己开发调度策略,管理硬件。

进程、线程、协程

进程是程序的运行实例,将程序加载进内存,就产生了一个进程。
线程是进程的一部分,也是操作系统调度给CPU的最小执行单元,一个进程可以包含多个线程。线程可以分为用户态线程和内核态线程,其分别运行于操作系统用户空间和内核空间。用户态和内核态的出现是为了隔离资源的使用权限。用户态的线程无法直接操作硬件等资源,如想要操作硬件资源,需要切换到内核态去完成执行。用户态到内核态的切换有3种情况:系统调用,中断,异常。当执行完内核态的代码后,会自动回退到用户态继续执行其代码。
协程(co-routine)可以理解成是更加细粒度的线程,其调度策略由应用程序自行指定。Go语言中的协程称作goroutine,其主要在用户态下产生并被调度。协程是依托于线程执行的,只有将协程绑定到线程上,才能执行其代码。

通常情况下32位操作系统中,进程的虚拟内存空间可以达到4GB、64位操作系统下其虚拟内存空间会很充裕。不同进程间的资源不可见。操作系统在调度进程切换时会保存相当多的上下文信息,极其耗费CPU时间。

线程的栈空间大概为2M,该大小无法修改,同一个进程中的共享数据对该进程中的所有线程都是可见的。若想让一个线程内的全局变量只对该线程内可见,可以使用Thread Local Storage(线程局部存储)TLS技术。线程的切换需保存线程的上下文信息(前者说单个进程内的线程切换时;当多个进程间的线程切换时,还要保存进程的相关信息)同时由用户态转换为内核态做切换工作,十分耗费CPU时间。

Go协程的栈空间是可变的,可由2k(默认初始值)增长到1GB(64位操作系统)或250M(32位操作系统)。栈扩容后会将原栈空间的所有值全部复制到新栈中,栈中变量的地址会发生改变。这也许间接的导致了Go语言的普通指针不支持运算操作。协程间可以共享进程的全局变量。协程间切换由协程调度器在用户态完成,只需保存几个寄存器的值即可,非常省时高效。

3种线程模型

1:1 1个用户态线程与1个内核态线程绑定
N:1 多个用户态线程与1个内核态线程绑定
M:N 多个用户态线程与多个内核态线程绑定

Go 语言协程调度器使用的是M:N的模型,即由Go协程调度器将M个协程(非线程)与N个内核线程进行动态绑定,执行代码,N一般为CPU的逻辑核心数。
Go语言淡化了线程的概念。写代码时只能感知到协程的存在,当然这对程序设计是没有任何影响的。

GM调度模型

早期的Go语言版本,使用的是GM调度模型。G代表goroutine,M代表machine(即内核线程)。同时维护了一个全局的G队列。这个队列在访问时需要加一把大锁。

加锁的全局运行队列 [G,G,G,G,G,G,G,G,G,G,G,G,G]

G1-----------\
G2-----------|---------------M0
G3-----------/
G4---------------------------M1
G5---------------------------M2
G6---------------------------M3
如上所示,6个goroutine绑定到了4个内核线程上。若G6在M3线程上运行结束,则M3会去全局队列解锁,然后拿来一个新的G绑定,执行。若G1在运行时发生了阻塞则M0就进入了阻塞状态,G2,G3需等到阻塞结束才有机会被执行到。可以看出早期的Go调度器是非常粗糙的,其调度效率并不高。

GMP调度模型

后期的Go语言改进了GM调度模型,加了一个虚拟的逻辑处理器P(procese),G仍代表goroutine,M仍代表machine(即内核线程)。

加锁的全局运行队列 [G,G,G,G,G,G,G,G,G,G,G,G,G]

G:goroutine

M0<->P0: 加锁的本地运行队列 [G,G,G,G,G]

M1<->P1: 加锁的本地运行队列 [G,G]

M2<->P2: 加锁的本地运行队列 [G]

M3<->P3: 加锁的本地运行队列 [G,G,G]

某一时刻,1个P只能与1个M进行绑定。一个G可能在多个P的本地运行队列中流转,也有可能被移动到全局运行队列。
M在执行G的时候,会优先从与其绑定的P的结构中查看其runnext指针是否指向一个可被执行的G,若该指针有效,则直接执行该G,否则说明runnext指向空(0),此时则去P的本地队列中去查找是否有可执行的G,若有则加锁拿一个G后解锁,然后去执行,若没有则去加锁的全局队列中取寻找是否有可用的G,若有,则从全局队列中窃取,按P的个数等分全局运行队列G数量的G,窃取来的G的数量不超过P的本地队列的长度的一半,而后执行。若全局队列中没有G,则去随机选择一个P,在其本地运行队列窃取G,数量为目标队列中已有的G的数量的一半,执行。若其他P中仍没有G,则检查网络协程是否有可以被调度的G。为了保证全局队列中的G会被执行到,在P被调度61次以后,会将本地队列中一般的G加入全局队列,并从全局队列中获得G加入本地队列。若M绑定的P没有可执行的G,则将M与P解绑,M进入阻塞状态。

main与g0,m0,sysmon

main协程是Go程序的主协程,main方法是Go程序的入口。在每个M中有一个g0协程,g0协程其实就是负责调度策略的协程,g0协程会调度其他的G执行。Go程序在启动时,会有g0协程,负责GC的协程,以及一个额外的负责监控Go状态的sysmon线程运行,而后才是main协程及用户的其他协程。g0协程是被一个m0线程所启动的,g0被启动后,m0便负责M应有的责任。

Go调度器

Go 语言协程调度器支持抢占式调度和协作式调度,默认是抢占式调度策略。在Go语言的发展中,调度器的调度算法及实现仍有很大的提升空间,相信Go语言会越做越好。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

metabit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值