摘要
Go 的调度机制相当于我们微服务里的基础组件。很多运行时操作都涉及到了调度的关联。本文会细聊调度概念,策略,以及它的机制。当然,也少不了最常提及的 GMP 模型。
一、调度是什么?
计算机的 资源 是 有限 的,像 CPU,内存都是固定的。但是同一时间可能会有 多个任务 要去完成,比如操作系统的定时监控,用户程序的运行等。
怎么让资源 最大化 的完成任务,这是调度需要考虑的关键点。
调度可以理解为一个指挥员,指导我们的程序按照一定的规则去获取资源,然后去执行里面的指令。
那么,一般的规则有哪些呢?
常见的调度策略有 2 种,一种是 协作式调度 ,会让程序顺利的完成自己的任务,再把资源腾出来给其他程序使用。
另一种是 抢占式调度 ,也就是让程序按一定的时间去占有这些资源,时间到了就被迫让出现有资源,给其他的程序轮流使用。
协作式调度
有利于程序专注的完成自己的任务,但也可能会造成其他程序一直
饿死
,得不到执行。
抢占式调度 有利于程序在资源的利用上 雨露均沾 ,但是在不断的切换过程中,将会使得程序原本 10 ms 能完成的事,不得不 延迟 多几 ms。
注:Linux 操作系统也是采用了抢占式调度,并且使用了 CFS:完全公平调度算法。通过对程序大致的运行时间来 平衡调度 ,让越没有执行过的程序,越快被调度到。
当前大多数操作系统都是采用 抢占式调度 来执行程序的,毕竟很多操作系统都是面向用户,需要很高的 响应速度 ,而且只要切换程序的周期够短,例如 50ms,那对于用户来讲,就像没切换一样。
二、golang 的调度
上面提及到 抢占式调度 会有个频繁切换的过程,在切换时,需要不断的 保存或恢复 上下文信息。
而这会涉及到操作系统内核态和用户态的 切换 ,性能损耗会很大。
对此,golang 实现了属于自己的调度模型,采用了 基于协作的抢占式调度 。之所以是"协作"的,是因为 Go 的调度时机是由用户 自己设置 的,而这里的用户指的是 golang 的运行时 runtime 。
它会在下面的事件发生时进行调度触发:
- 使用关键字 go
- 垃圾回收
- 系统调用,如访问硬盘
- 同步阻塞调用,如 使用 mutex、channel
如果上面什么事件都没发生,则会有 sysmon 来监控 goroutine 的运行情况,对长时间运行的 goroutine 进行 标记 。一旦 goroutine 被标记了,那么它就会下次发生函数调用时,将自己挂起,再触发调度。
这里需要说明下的是,runtime 它相当于 Java 的虚拟机,负责了 Go 的很多东西,例如 调度 , 垃圾回收、内存管理 等,可以说是涵盖了 Go 的基础引擎了。
更重要的是 runtime 是运行在 用户态 上的,相当于 Go 的调度是在用户态这一层进行的。
这样,每当 Go 有调度产生时,就不会伴随着用户态和内核态的切换,而是像前面提到过的策略那样去触发调度,这就 降低 了并发时的内核态与用户态的 切换成本 了。
三、golang 的 GPM 模型
为了实现 golang 的调度,golang 抽象出了三个结构,也就是我们常见的 G、P、M 。
G:也就是协程 goroutine,由 Go runtime 管理。我们可以认为它是用户级别的线程。
goroutine 非常的 轻量 ,初始分配只有 2KB,当栈空间不够用时,会自动扩容。同时,自身存储了执行 stack 信息、goroutine 状态以及 goroutine 的任务函数等。
P:processor 处理器。P 的数量默认跟 CPU 的核心数一样,如果是多核的 CPU,则会有多个 P 会被创建。
每当有 goroutine 要创建时,会被添加到 P 上的 goroutine 本地队列 上,如果 P 的本地队列已满,则会维护到 全局队列 里。
在进行调度时,会 优先 从本地队列获取 goroutine 来执行。
如果本地队列没有,会从其他的 P 上 偷取 goroutine。
如果其他 P 上也没有,则会从 全局队列 上获取 goroutine。
这样通过上面的策略,就能尽 最大努力 保证有 goroutine 可 运行 。
M:系统线程。在 M 上有调度函数,它是真正的 调度执行者 ,M 需要 跟 P 绑定 ,并且会让 P 按上面的原则挑出个 goroutine 来执行。
M 虽然从 P 上挑选了 G 执行,但 M 并不保存 G 的上下文信息,而是 G 自己保存了相关信息,这样有利于转移到其他 M 上,在不同的 M 上运行。
GPM 模型的优势点在于 G 包含了 执行任务 相关信息,M 提供了 执行环境 ,并且有调度机制。而 P 则是他们两者的粘合剂。
假如没有 P 。那么 M 就会有争夺 G 的 竞争问题 ,并且 M 的数量会不可控,会出现 过多的 M 去处理 G。
一旦超过了 CPU 的核心数,那么就会将性能耗费在上下文切换过程中。
有了 P 这一层后,M 优先从 P 的 本地队列 获取 goroutine, 减少并发 竞争。并且保证了最多跟 CPU 核心数一样的 goroutine 数量在并行运行,充分利用了 多核优势 ,又不被滥用。
总结
相信看过本文后,各位对 Golang 的调度有了一定的了解。正是因为基于协作的抢占式调度和 GMP 模型,Golang 的高并发高性能才有了底层保障。当然,大伙也可以深入到源码去分析这些调度机制,这样离大神就更近一步了 ㋡...