之前在深圳Gopher微信群里回答一个问题,将 Go 调度比喻成程序员的工作场景,就把一个非常费解的问题极简化了,拖延两个月后总结成本文。
Go语言语言级并发最小逻辑单元是 goroutine,goroutine 有类似线程的特性但调度起来比线程轻量很多。其轻量程度类似于 Erlang 中的“纤程”,一个 Erlang 虚拟机进程可以承载数万“纤程”。一个操作系统线程也可以承载数万 goroutine,相比之下线程和进程几乎都处于一个非常重的数量级。
我们可以在多线程或多进程比较昂贵的场景中创建很多低成本的 goroutine 来减少运行时操作系统调度损耗,或者把事件式异步编程这种开发时间长而质量脆弱的并发模型改用 goroutine 以同步的方式用几乎相同的运行时成本解决。
因为目前的操作系统实现是只提供了原生的进程和线程,甚至 Linux 因为进程和线程区别不大而把线程实现为共享内存空间的进程而已,那么就需要编程语言自身来实现这些机制。
Go 的调度器是实现这种机制的核心,其中有四种抽象模型非常重要,分别是 G、P、M、S :
G:即 goroutine 的栈、寄存器等信息,当 P 把一个 G 送到 M 时 goroutine 开始得到运行资源。
P:Processor 维护一个自己的 G 队列,并找到可用的 M 来执行 G ,以前在一个进程中默认创建 1 个 P,现在调度器成熟后 P 数量默认和 CPU 核心数相同。
M:S 会创建多个操作系统线程并抽象为 Machine ,M 在执行系统调用时可能会阻塞,默认 M 的数量大于 P 的数量,这样某个 M 在执行某个 G 阻塞后,P 可以找另一个空闲的 M 继续跑其它的 G。
S:进程中唯一的调度器 Schedule,负责创建监控线程和 G、P、M 以及分配和维护资源队列。
我尝试把这四种抽象代入到我们平时工作的场景:G,是要实现的需求;P,是程序员;M,是程序员用的开发机或服务器;S,是老板,它先招一个监控者,再负责配置 M 、招聘 P、并创建 G 给 P 执行。
这个公司怎么运作?首先从老板开始:老板创建公司后,买了机器招了程序员,并创建了一个监控者,这样就可以开始创建需求了。
需求从老板那里创建出来后,或者直接放到某程序员的待执行队列,或者放在一个公共需求队列让程序员自己取。程序员在自己的待执行队列或公共需求队列拿到某需求后,找到一台空闲的机器就可以开始工作起来。需求做完了程序员就把需求释放,一个时间片做不完的需求程序员会重新放到待执行队列中。
这个公司的程序员非常热心,如果自己的需求和公共需求都完成了,那他会去其它程序员那里看看它们的需求队列还有没有等待完成的需求,如果有就拿一个需求过来自己做。
如果程序员用的机器被阻塞式占用,程序员会把这台机器和阻塞的需求关联起来,并找另一台空闲的机器继续做其它需求。如果没有机器用了,程序员会向老板要求更多机器。阻塞的需求重新开始执行后,会重新加入程序员的需求队列,而阻塞的需求占用过的机器会变得无所事事而重新加入空闲机器队列。
在这个公司,老板 S 只有一个,程序员 P 数量相对固定,机器 M 的数量大于 P 以便 P 可以随时找到空闲的 M,这样的公司可以完成无数的需求 G。
现实中,我在一台 4 核心 CPU 服务器上执行一个服务,P 数量为 4,M 数量为 10,G 以每秒 20 个以上的速度不断创建和销毁数量维持在几千个。
如果觉得 Go 调度比较费解的同学,可以尝试理解了这个程序员工作模型再去研究。如果不是一头就扎进细节,那么我相信本文能帮到你。
2016-11-08 深圳
预祝大家光棍节快乐!