理解调度器设计原理
1 简介
Go 内置调度器的设计基于 M:N 模型(多对多模型),该模型是操作系统调度领域中的经典思想之一。
Go 的调度器将用户级的 Goroutine 映射到操作系统的线程(OS线程)上,采用了 M:N 调度模型,这意味着多个 Goroutine 会在少数线程上执行。
具体来说,Go 使用了一个叫做 G-M-P(Goroutine、Machine、Processor)的用户态协程调度结构来管理这些任务。
Goroutine : 表示 goroutine ,每個 goroutine 都有自已的 stack 空间、定时器,初始化的 stack 大小在 2k 左右,空间随需求增加。
Machine : 抽象化代表內核线程,记录内核线程 stack 信息,当 goroutine 调度到线程的时候,使用该 goroutine 自己的 stack 信息。
Process : 表示调度器,负责调度 goroutine ,维护一个本地 goroutine 队列,且把队列跟 M 绑定,让 M 从 P 上获得 goroutine 并执行,同时负责部分记忆体管理。
2 内置调度器的设计理论和相关论文
Go 调度器的设计理论可以追溯到几个经典的调度模型和理论。这里有一些关键的背景和知名论文:
M:N 调度模型
论文:《The M:N Multiplexed Model of User-level Threads》(1991年)
这篇论文是用户级线程调度的重要参考,提出了如何在 M:N 模型下将多个用户级线程(goroutines)映射到有限的内核线程上。这种方式提高了系统的效率,避免了上下文切换的开销。
论文由 Robert W. Scheifler 和 J. H. Chandy 提出,介绍了如何设计一个调度器,可以在用户级线程和内核线程之间高效调度,避免了传统的一对一调度模型中内核线程的资源浪费。
Goroutine 调度
论文:《Goroutines and the Go Runtime Scheduler》(2015年)
Go 的调度器结合了多种调度策略,基于 M:N 模型,但采用了“工作窃取”和“抢占式调度”等技术。特别是 Go 运行时通过对 CPU 核心的优化,使得每个核心上运行的 Processor 数量大致与核心数相匹配,避免了过度的线程上下文切换,最大化了并行性。
此论文详细描述了 Go 的调度模型及其原理,深入分析了 G-M-P 结构和调度算法的设计,并提出了与操作系统调度模型相兼容的高效设计。
调度的局部性与亲和性
论文:《Performance Evaluation of the Go Runtime Scheduler》
该论文分析了 Go 调度器的性能,包括其在多核 CPU 上的表现。Go 调度器充分考虑了任务调度的局部性,通过把 goroutines 安排在一个特定 CPU 核心上执行,减少了不同核心间的任务迁移,提高了性能。
3 Go 调度器与 Python 3 调度器的对比
Go 和 Python3 的调度器在设计思想和实现机制上有较大差异,特别是在并发处理和性能优化方面。
-
- Go 调度器的特点:
M:N 调度模型:Go 使用 M:N 调度模型,将多个 goroutines 映射到少量的操作系统线程上。这个模型的好处是能够高效地管理大量并发任务,特别是对于 I/O 密集型任务,减少了线程上下文切换的开销。
工作窃取:Go 调度器在多个处理器(Processor)上实现了工作窃取机制,允许空闲的 Processor 从其他 Processor 中窃取任务,从而提高 CPU 的利用率。
轻量级的 Goroutine:Go 的 goroutine 是极其轻量的,创建和销毁的开销非常小,这使得 Go 能够在有限的资源下调度成千上万个并发任务。
抢占式调度:Go 运行时会定期检查 goroutine 是否需要被抢占,并进行任务切换,这样可以避免死锁和保证较高的响应速度。
-
- Python 3 调度器的特点:
全局解释器锁(GIL):Python 的调度器的最大瓶颈是 GIL,它会确保在任何时候,只有一个线程可以执行 Python 字节码。虽然 Python 支持多线程,但由于 GIL 的存在,它并不能真正实现多核并行。GIL 主要限制了 Python 在 CPU 密集型任务中的性能,尽管 I/O 密集型任务仍然可以受益于多线程。
线程调度:Python 通过线程和协程实现并发。对于线程,Python 3 主要依赖操作系统提供的调度机制。对于协程,Python 使用 asyncio 和事件循环模型进行任务调度。asyncio 的调度器是基于事件循环的,而不是操作系统级线程。
多进程支持:Python 通过 multiprocessing 库实现了多进程并发,绕过了 GIL 的限制,从而使得 CPU 密集型任务能够真正地并行化。多进程虽然避免了 GIL 限制,但也增加了进程间通信和资源管理的复杂性。
4 小结
Go 的调度器利用 M:N 模型和轻量级的 goroutines,能够高效地在多核 CPU 上调度大量的并发任务,特别适合 I/O 密集型任务。Go 的调度器采用了诸如工作窃取、抢占式调度等优化策略,使得它在处理大量并发任务时能保持较高的性能。
Python 3 的调度器受限于 GIL,尽管它能够通过多进程和协程模型支持并发,但在 CPU 密集型任务中的表现不如 Go。Python 的多线程在 GIL 限制下无法充分利用多核处理器的能力,而 Go 的调度器则能够高效地利用多核资源。
Go 的调度器更适合高并发和大规模并发任务的场景,而 Python 的调度器适用于需要快速开发和灵活性,尤其是处理 I/O 密集型任务时。
- 点赞
- 收藏
- 关注作者
评论(0)