golang学习篇--调度器GMP

golang 调度器的由来

单进程时代

最早期一切程序都是串行的,不需要调度器
在这里插入图片描述
早期系统是单线程的,单线程的系统在执行多个任务的时候,因为是单一流程执行,所以cpu在处理C任务的时候其他任务都会处于阻塞状态。

多进程/线程时代

后来进入多进程/线程时代的时候就有了调度去,解决了阻塞问题。
在这里插入图片描述
但是当进程拥有太多的资源的时候,进程的创建、切换、销毁都会占用很长的时间,cpu虽然利用起来了,但是如果进程太多,cpu大多数时间都在进行进程的调度。

在linux下cpu对进程和线程的态度是一样的。
在这里插入图片描述
如何来提供cpu的利用率呢

协程时代

多进程/线程解决了系统的并发能力,但是在当今的互联网高并发的情况下,为每个任务创建一个进程/线程是不现实的,因为要消耗大量的内存和调度时cpu的高消耗。

  • 进程的栈空间在几 MB 到几十 MB 之间。
  • 线程的栈空间在几 KB 到几 MB 之间。
  • 协程的栈空间在几 KB。
    其实一个线程分为内核态线程和用户态线程。一个用户态线程必须要绑定一个内核态线程,CPU并不知道有“用户态线程”的存在,它只知道它运行的是一个“内核态线程”(Linux的PCB进程控制块)。
    在这里插入图片描述
    我们再去细化去分类一下,内核线程依然叫“线程(thread)”,用户线程叫“协程(co-routine)".
    在这里插入图片描述
    如图一个线程绑定一个协程,那么N个协程能否绑定一个线程呢?当然是可以的。
N:1的关系:

N个协程绑定一个线程
优点:是协程在用户态线程既可以完成切换,不会陷入到内核态,这总切换非常的轻量快速。
缺点:如果某一个协程阻塞,会造成线程阻塞,该线程的其他协程都无法执行,丧失了并发能力。
在这里插入图片描述

1:1

1个协程绑定一个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点
缺点:协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵了。
在这里插入图片描述

M:N

M个协程绑定N个线程,是N:1和1:1的结合,克服了上诉的两种模式的缺点。
在这里插入图片描述
协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。

Goroutine

go提供了更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程上,即使有协程阻塞,该线程的其他协程也可以被runtime调度到其它可以运行的线程上执行。
go中,协程被称为goroutine,它非常轻量,一个goroutine一般只有几kb,这几kb足以让goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。
虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。

goroutine的特点:

  • 占用内存更小(几kb)
  • 调度更灵活(runtime调度)
go最早的调度模型GM(被废弃)

g:goroutine 协程
m:thread 线程

gm模型是如何调度的

在这里插入图片描述
M(线程)有多个,线程在执行的时候需要访问全局的G队列,这样多个线程访问同一个资源需要通过加锁来保证互斥/同步,所以全局的G队列是有互斥锁进行保护的。
GM调度器有几个缺点:

  • 创建,销毁,调度goroutine的时候每个M(线程)都要去获取锁,这就形成了激烈的锁竞争。
  • 如果有协程阻塞的时候M(线程)转移goroutine的时候会造成延迟和额外的系统负载。
  • 系统调度线程的时候,过于频繁的线程阻塞和取消阻塞造成了系统额外的开销。
go现在的调度模型GMP

现在的调度模型除了M(线程)和Goroutine之外,多了一个Processor(处理器)。
g:goroutine 协程
m:thread 线程
processor:它包含了运行goroutine的资源,如果线程想运行gorotine,必须先获取p,p中还包含了可运行的G队列

在GMP模型中,线程是运行goroutine的实体,调度器的功能是把可运行的gorotine分配到工作线程上。
在这里插入图片描述

  • 全局队列(global queue):存放等待运行的G
  • P的本地队列: 同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个,新创建了一个G1时,G1优先加入到p的本地队列中,如果队列满了,则会把本地队列中的一半的G移动到全局队列。
  • P列表:所有的P都在程序启动的时候创建,并保持在数组中,最多有GOMAXPROCS个,GOMAXPROCS可以通过配置来设定。
  • M: 线程想运行任务就的获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或者从其他P的本地队列偷一般放到自己的P的本地队列。M运行G,当前G执行完以后M会从P获取下一个需要执行的G。
  • goroutine调度器和系统调度器是通过M(线程)结合起来的,每个M都代表一个内核线程,系统调度器负责把内核线程分配到CPU的核上执行。
    P和M的个数
  • P的数量由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。
  • M的数量,go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略,也可以通过SetMaxThreads函数,设置M的最大数量。一个M阻塞了,会创建新的M
    P和M什么时候创建
  • P在系统运行时会根据GOMAXPROCS的个数创建n个P
  • M在没有足够的M来关联P并运行其中的G的时候,例如所有的M此时都阻塞,P队列中又有很多就绪的任务,这时回去寻找空虚的M,没有空闲的M就回去创建新的M出来绑定当前的P来消耗任务。
调度策略

线程频繁的创建,销毁是一个极大的消耗,go设计了复用线程。

  1. work stealing机制(偷)
    当前线程无可用的G时,会尝试从其他线程绑定的P中偷取G,而不是销毁线程。
  2. hand off机制
    当本线程因为G运行阻塞时,线程会释放绑定的P,把P转移给其他闲置的线程执行。

利用并行
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。
抢占
在coroutine中要等待一个协程主动让出cpu才能执行下一个协程,在go中一个goroutine最多占用cpu 10ms,防止其他goroutine被饿死。
全局G队列
在新的调度器中依然有全局G队列,但是被弱化了,只有M无法从其他P队列中无法偷取到G的时候,才会从全局G队列中获取G。

go func() 调度流程

在这里插入图片描述

  1. 我们通过go func() 来创建一个goroutine;
  2. 有两个存储G的队列,一个p本地队列和一个全局队列,新创建的G会先保存在p的本地队列中,如果p的本地队列已经满了就会保持在全局队列中。
  3. 一个线程绑定一个p,M会从P的本地列表弹出一个可执行的G来执行,如果P的本地列表为空,就会从其他M绑定的P中偷取一个G,如果其他M的P中没有准备就绪的G就会从全局队列中拉去G。
  4. 一个M从P从调度可执行的G是一个循环的过程。
  5. 当M执行G发送syscall或阻塞的时候,M会阻塞,如果当前M的P队列中有准备就绪的G等待执行,runtime会把当前M的P解绑,然后(创建出一个/寻找一个空闲的)M来服务这个P
  6. 当M因为执行阻塞任务结束时,由于M的P本队队列让摘除掉了,这个时候G会获取一个空闲的P,如果获取不到P,这个线程就会变成休眠状态,加入到空闲线程中,然后这个G会被放入到全局队列中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值