摘要
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 的高并发高性能才有了底层保障。当然,大伙也可以深入到源码去分析这些调度机制,这样离大神就更近一步了 ㋡...