Go语言学习
  • README
  • Go 基础
    • go语言介绍
    • go语言主要特性
    • go内置类型和函数
    • init函数和main函数
    • 下划线
    • iota
    • 字符串
    • 数据类型:数组与切片
    • 数据类型:byte、rune与字符串
    • 变量的5种创建方式
    • 数据类型:字典
    • 指针
    • 数据类型:指针
    • 类型断言
    • 流程控制:defer延迟执行
    • 异常机制:panic和recover
    • 函数
    • go依赖管理
    • go中值传递、引用传递、指针传递区别
  • 标准库
    • Go net/http包
  • 数据结构
    • 哈希表
      • 为什么对 map 的 key 或 value 进行取址操作是不允许的
  • Gin
    • gin 快速开始
    • gin-swagger用法
  • Go 进阶
    • Go 指针
    • Go 中的 GC 演变是怎样的?
    • Go 的堆和栈
  • 面向对象
    • make 和 new 的区别
    • new(T) 和 &T{} 有什么区别?
  • 并发编程
    • Channel
    • Go语言 CSP 并发模型
    • GMP 模型原理
      • GMP 模型为什么要有 P ?
    • Go 协程池(goroutine pool)
    • Go语言常见的并发模式
    • Go并发实践:主动停止goroutine
  • 最佳实践
    • 发布Go语言模块
  • 软件包
    • 常用的GoLang包工具
    • Go的UUID生成
    • 现代化的命令行框架Cobra
    • 配置解析神器Viper
    • Go发送邮件gomail
    • Go反射框架Fx
    • NSQ消息队列的使用
    • Go爬虫框架colly
    • grpc-go 的安装和使用
Powered by GitBook
On this page
  • 1. 什么是 GMP ?
  • 2. GMP 核心
  • 3. 调度器的设计策略
  • 延伸阅读

Was this helpful?

  1. 并发编程

GMP 模型原理

goroutine 的调度

golang调度的机制用一句话描述: runtime准备好G、P、M,然后M绑定P,M从各种队列中获取G,切换到G的执行栈上并执行G上的任务函数,调用goexit做清理工作并回到M,如此反复。

1. 什么是 GMP ?

  • G:Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在 32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。

  • M:Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常

  • P:Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。

2. GMP 核心

两个队列

在整个 Go 调度器的生命周期中,存在着两个非常重要的队列:

  • 全局队列(Global Queue):全局只有一个

  • 本地队列(Local Queue):每个 P 都会维护一个本地队列

当你执行 go func() 创建一个 goroutine 时,会优选将该协程放入到当前 P 的本地队列中等待被 P 选中执行。

但若当前 P 的本地队列任务太多了,已经存放不下了,那么这个 goroutine 就只能放入到全局队列中。

两种调度

一个协程得以运行,需要同时满足以下两个条件:

  1. P 已经和某个线程进行绑定,这样才能参与操作系统的调度获得 CPU 时间

  2. P 已经从队列中(可以是本地队列,也可以是全局队列,甚至是从其他 P 的队列)取到该协程

第一个条件就是 操作系统调度,而第二个其实就是 Go 里的调度器。

操作系统调度

假设一台机器上有两个 CPU 核心,意味着,同时在同一时间里,只能有两个线程运行着。

可如果该机器上实际开启了 4 个线程,要是先执行一个线程完再执行另一个线程,那么当某一个线程因为一些阻塞性的系统调用而阻塞时,CPU 的时间就会因此而白白浪费掉了。

更合适的做法是,使用 操作系统调度策略,设定一个调度周期,假设是 10ms (毫秒),那在一个周期里,每个线程都平均分,都只能得到 2.5ms 的CPU 运行时间。

可如果机器上有 1000 个线程呢?难道每个线程都分个 0.01 ms (也就是 10 微秒)吗?

要知道,CPU 从 A 线程切换到 B 线程,是有巨大的时间浪费在线程上下文的切换,如果切换得太频繁,就会有大量的 CPU 时间白白浪费。

因此,通常会限制最小的时间片的长度,假设为 2ms,受此调整,现在调度周期就会变成 2*1000 = 2s 。

Go调度器

在 Go 中需要用到调度的,无非是如下几种:

将 P 绑定到一个合适的 M

P 本身不能直接运行 G,只将 P 跟 M 绑定后,才能执行 G。

假设 P1 当前正绑定在 M1 上运行 G1,此时 G1 内部发生了一次系统调度后,P1 就会与 M1 进行解绑,然后再从空闲的线程队列中再寻找一个来绑定,假设绑定的是 M2,可如果没有空闲的线程呢?那没办法,只能创建一个新的线程再进行绑定。

绑定后,就会再从本地的队列中寻找 G 来执行(如果没找到,就会去其他队列找,上面已经讲过,不再赘述)。

过了一段时间后,之前 M1 上 G1 发生的系统调用结束后,M1 会去找原先自己的搭档 P1(它自己会记录),如果自己的老搭档也刚好空闲着,就可以再次合作进行绑定,接着运行 G1 未完成的工作。

可不幸的是,P1 已经找到了新的合作伙伴 M2,暂时没空搭理 M1 。

M1 联系不上 P1,只能去寻找有没有其他空闲的 P ,如果所有的 P 都被绑定着,说明现在任务非常繁重,想完成任务只能排队慢慢等。

于是,M1 上的 G1 就会被标记为 Runable ,放到全局队列中,而 M1 自身也会因为没有 P 可以绑定而进入休眠状态,如果长时间休眠等待 则会 GC 回收销毁

为 P 选中一个 G 来执行

P 就像是一个流水线工人,而 P 的本地队列就是流水线,G 是流水线上的零件。而 Go 调度器就是流水线组长,负责监督工人的是不是有在努力的工作。

完成一个 G 后,P 就得立马接着从队列中拿到下一个 G,继续干活。

遇到手脚麻利的 P ,干完了自己的活,本想着可以偷懒一会,没想到却被组长发现了,立马就从全局队列中拿了些新的 G 交到 P 的手里。

天真的 P 以为只要把 全局队列中的 G 的也干完了,就肯定 能休息了吧?

当 P 又快手快脚的把全局队列中的 G 也都干完的时候,P 非常得意,心想:终于可以休息会了。

没想到又被眼尖的组长察觉到了:不错啊,小 P,手脚挺麻利的。看看其他人,还那么多活没干完。真是拖后腿。可谁让咱是一个团队的呢,要有集体荣誉感,你能者多劳。

说完,就把其他人的 G 放到了我的工作上。。。

3. 调度器的设计策略

复用线程

避免频繁的创建、销毁线程,而是对线程的复用。

1)work stealing 机制

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

2)hand off 机制

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

利用并行

GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。

抢占调度

在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这是协作式抢占调度。

而在 go 1.14+ ,Go 开始支持基于信号的真抢占调度了。

全局 G 队列

在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。

延伸阅读

PreviousGo语言 CSP 并发模型NextGMP 模型为什么要有 P ?

Last updated 2 years ago

Was this helpful?

image0

[典藏版] Golang 调度器 GMP 原理与调度全分析