goroutine和GPM调度

目录

一、Go 语言的协程 goroutine

1. goroutine概念

2. goroutine特点

二、m:n调度技术

三、goroutine 调度器的实现

1. 被废弃的调度器

2.新调度器GPM设计思想

3.P和M的数量和创建

数量

创建

4.调度器的设计策略

线程复用

利用并行

抢占

全局 G 队列

5.GPM调度流程

6.调度器的生命周期


一、Go 语言的协程 goroutine

1. goroutine概念

goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上;
即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上;

2. goroutine特点

占用内存更小(只占几 KB,可伸缩的,runtime会自动为goroutine分配更多内存);
调度更灵活 (runtime 调度);


二、m:n调度技术

m:n调度的技术,m个goroutine(G)调度到n个OS线程(M)上运行;

一对一和一对多的缺点;


三、goroutine 调度器的实现

1. 被废弃的调度器

 2012年重新设计之前的调度器,只有GM;

特征:

1)M 想要执行、放回 G ,都必须访问全局 G 队列;

2)M 有多个,即多线程访问同一资源(即G队列)需要加锁进行保证互斥 / 同步。
 

老调度器有几个缺点:

1)激烈的锁竞争;

2)M 转移 G 会造成延迟和额外的系统负载;

比如当 G 中包含创建新协程的时候,M 创建了 G’;为了执行 G,需要把 G’交给 M’,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’;

3)系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销;

2.新调度器GPM设计思想

GPM是Go语言运行时层面(用户态)的实现,是go语言自己实现的一套调度系统,区别于操作系统调度OS线程(内核态)。

1)G:Goroutine,协程;

包含goroutine信息和与P的绑定信息;

2)P:Processor,调度器;

所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个;

管理和调度一组goroutine队列,当自己的队列消费完了去全局队列里取,全局队列里消费完了会去其他P队列里取;

2)M:Machine,线程映射;

Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系;

线程想运行任务就得获取 P,从 P 的本地队列获取 G;

4)GQ:全局队列(Global Queue);

5)LQ:P 的本地队列;

存的数量有限,不超过 256 个;

满:新建 G’时,G’优先加入到 P 本地队列,如果队列已满,则把队列中前一半的 G和新创建的G移动到全局队列;

空:P 队列为空时,M 尝试从全局队列拿一批 G 【负载均衡,min(全局队列长度/P的数量  + 1, 全局队列长度/2)】放到 P 本地队列,或从其他 P 本地队列后一半放到自己 的P 本地队列;

协程和线程是通过P结合起来的(G - P -M), 线程想运行任务就得获取 P,从 P 的本地队列获取 G;

Goroutine 调度器和 OS 调度器是通过 M 结合起来的(goroutine调度器 - M - OS调度器);

3.P和M的数量和创建

数量

1)P 的数量:

由GOMAXPROCS决定,可配置;

这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行;

2)M 的数量:

go 语言限制:go 程序启动时,会设置 M 的最大数量,默认 10000,但是内核很难支持这么多的线程数;

SetMaxThreads 函数,可设置 M 的最大数量;

一个 M 阻塞了,会创建新的 M;

3)M与P数量的关系:

M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M;

即使 P 的默认数量是 1,也有可能会创建很多个 M 出来;

创建

1)P 何时创建:

在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

2)M 何时创建:

没有足够的 M 来关联 P 并运行其中的可运行的 G时,会创建新的M;

比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M;

4.调度器的设计策略

线程复用

work stealing 机制

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

hand off 机制

当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

利用并行

GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。

抢占

在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程;

在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死;

全局 G 队列

在新的调度器中依然有全局 G 队列,但功能已经被弱化了;

当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G;

5.GPM调度流程

从上图我们可以分析出几个结论:

1)我们通过 go func () 来创建一个 goroutine;

2)有两种存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列;新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

3)G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系;M 会从 P 的本地队列弹出一个 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G;

4) 一个 M 调度 G 执行的过程是一个循环机制

5)当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞;如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach);然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;

6)当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列;如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

6.调度器的生命周期

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值