面试题答案
一键面试调度原理
- M:N 调度模型:Go 语言采用 M:N 调度模型,即 M 个操作系统线程(M)对应 N 个 goroutine(N)。每个 M 被称为一个
machine
,N 个 goroutine 被调度到这些 M 上执行。 - Goroutine 调度器:包含全局运行队列(GRQ)、本地运行队列(LRQ)和调度器(Scheduler)。新创建的 goroutine 会优先进入 GRQ,当某个 P(
processor
,逻辑处理器,数量一般与 CPU 核心数相关)空闲时,会尝试从 GRQ 或其他 P 的 LRQ 窃取 goroutine 来执行。
性能瓶颈分析
- 全局运行队列竞争:当大量 goroutine 涌入 GRQ 时,多个 P 同时访问 GRQ 会产生竞争,导致调度效率降低。
- 本地运行队列不均衡:如果某些 P 的 LRQ 任务过多,而其他 P 空闲,会造成 CPU 资源利用不均衡。
- 系统调用阻塞:当 goroutine 执行系统调用时,对应的 M 会阻塞,此时 P 会调度其他 goroutine 到空闲的 M 上执行,但如果系统调用频繁,会影响整体性能。
优化措施
- 减少全局运行队列依赖:
- 合理分配任务:尽量在创建 goroutine 时,将其分配到特定的 P 对应的 LRQ 中,避免全部进入 GRQ。例如,可以使用
runtime.LockOSThread
函数将 goroutine 与特定的操作系统线程绑定,这样 goroutine 就会在该线程对应的 P 上执行。
- 合理分配任务:尽量在创建 goroutine 时,将其分配到特定的 P 对应的 LRQ 中,避免全部进入 GRQ。例如,可以使用
- 负载均衡:
- 动态调整任务:通过监控每个 P 的 LRQ 任务数量,当发现不均衡时,及时进行任务迁移。Go 调度器本身有一定的任务窃取机制,可进一步优化以提高效率。
- 使用工作池模式:可以创建一个工作池,将任务分发给工作池中的 goroutine,确保每个 goroutine 任务量相对均衡。
- 优化系统调用:
- 非阻塞系统调用:对于支持非阻塞的系统调用,尽量使用非阻塞方式。例如,在网络 I/O 中,使用
net.Conn
的非阻塞操作。 - 异步处理:将系统调用放在单独的 goroutine 中执行,使用 channel 来传递结果,避免阻塞主线程的 goroutine。
- 非阻塞系统调用:对于支持非阻塞的系统调用,尽量使用非阻塞方式。例如,在网络 I/O 中,使用
- 调整 GOMAXPROCS:
- 设置合适的值:
GOMAXPROCS
决定了同时运行的最大 P 的数量,一般设置为 CPU 核心数,可通过runtime.GOMAXPROCS
函数进行设置。如果设置过小,无法充分利用多核 CPU;设置过大,会增加调度开销。
- 设置合适的值: