线程模型有哪些?为什么 Go Scheduler 需要实现 M:N 的方案?Go Scheduler 由哪些元素构成呢?
参考回答
线程模型 是操作系统或编程语言设计中用来管理线程与硬件资源(如 CPU)之间关系的机制,常见的线程模型有以下三种:
- 1:1 模型:每个用户线程对应一个内核线程。
- N:1 模型:多个用户线程对应一个内核线程。
- M:N 模型:多个用户线程通过少量内核线程运行。
Go 的调度器(Scheduler)使用 M:N 模型,即将大量的 Goroutine 映射到少量的内核线程上执行。这种设计兼顾了高并发性能和系统资源效率。
Go Scheduler 的组成:
1. G(Goroutine):表示一个具体的协程,是需要调度的执行单元。
2. M(Machine):表示内核线程,用于实际执行 Goroutine。
3. P(Processor):代表逻辑处理器,负责 Goroutine 的分配和执行,与 M 绑定。
详细讲解与拓展
1. 常见线程模型详解
(1) 1:1 模型
– 每个用户线程直接映射为一个内核线程。
– 优点:线程可以并行利用多核 CPU。
– 缺点:线程切换开销大,资源使用率低。
示例:Java 的线程模型。
(2) N:1 模型
– 所有用户线程都由一个内核线程管理,调度在用户态完成。
– 优点:切换效率高,无需操作系统干预。
– 缺点:线程阻塞会导致所有线程阻塞,无法利用多核 CPU。
示例:早期的 Green Thread。
(3) M:N 模型
– M 个内核线程管理 N 个用户线程。
– 优点:结合了 N:1 和 1:1 的优势。能够利用多核 CPU,且切换开销低。
– 缺点:实现复杂,调度器需要高效管理线程。
示例:Go Scheduler、Erlang VM。
2. 为什么 Go Scheduler 选择 M:N 模型?
Go 语言的设计目标是支持 高并发 和 高性能,而 M:N 模型能够平衡资源利用率和调度效率,原因如下:
- 高并发支持:
Goroutine 是用户态线程,启动成本低,占用内存小(初始 2KB 栈),可以轻松支持数百万级并发。 -
避免阻塞:
- Goroutine 调度是协作式的,如果某个 Goroutine 阻塞,Go 运行时会将其他 Goroutine 调度到空闲的内核线程运行。
- 这避免了 N:1 模型的 “一阻塞全阻塞” 问题。
- 多核利用:
- M:N 模型允许多个 Goroutine 在多个内核线程上运行。
- 通过
runtime.GOMAXPROCS配置,可以有效利用多核 CPU。
- 调度效率:
- Goroutine 的上下文切换开销低,仅在用户态完成。
- 相比 1:1 模型,减少了频繁的内核切换开销。
3. Go Scheduler 的组成
Go 的调度器由以下三部分组成:
- G(Goroutine)
- 表示一个具体的 Goroutine,即需要运行的协程。
- 每个 G 结构体包含:
- 执行的函数或任务。
- 栈指针。
- 状态信息(如是否处于运行、休眠等)。
- 指向所属 P 的指针。
- M(Machine)
- 表示内核线程,是运行 Goroutine 的执行载体。
- 每个 M 绑定一个内核线程,负责执行从 P 中取出的 G。
- P(Processor)
- 表示逻辑处理器,是调度的核心单位。
- 每个 P 包含一个 Goroutine 队列,负责管理一组 G。
- P 和 M 一一绑定,每次只能有一个 M 处理 P 中的 Goroutine。
- 通过
runtime.GOMAXPROCS可以控制 P 的数量(即最大并行度)。
调度过程:
1. M 通过绑定一个 P 获取 Goroutine 队列中的任务。
2. 如果 P 队列为空,P 会从其他 P 的队列窃取任务(Work Stealing)。
3. 如果某个 Goroutine 阻塞,Go 运行时会创建新的 M 或将任务分配给其他 P。
4. Go 调度器的工作流程
简化版的调度流程如下:
1. 创建 Goroutine 时,将其放入 P 的本地队列。
2. M 从绑定的 P 中取出 Goroutine 执行。
3. 如果 Goroutine 阻塞(如 I/O 操作),M 会将其他任务交还给 P,由 P 重新调度。
4. 如果所有本地任务都已执行,P 会从其他 P 的队列中窃取任务(Work Stealing)。
5. 如果没有任务可运行,M 会阻塞或被回收。
总结
- 线程模型:
- 1:1、N:1 和 M:N 是三种常见线程模型。
- M:N 模型通过将大量用户线程映射到少量内核线程,既支持高并发,又避免阻塞问题。
- 为什么 Go 使用 M:N 模型:
- 支持高并发。
- 避免阻塞,提高资源利用率。
- 减少切换开销,提高性能。
- Go Scheduler 的组成:
- G:Goroutine,任务执行单元。
- M:Machine,内核线程。
- P:Processor,逻辑处理器,负责管理 Goroutine 队列。
这种设计使得 Go 可以高效地处理数百万级并发任务,同时充分利用多核 CPU 的性能。