type
status
date
slug
summary
tags
category
titleIcon
password
icon
calloutIcon
调度对象——Goroutine
- Goroutine是用户空间线程 (完全由Go运行时管理,没有OS介入)
- 轻量,开销小(更小的内存开销,更快的创建、销毁,上下文切换)
参考对比

为什么需要调度器?
由于Goroutine是用户层面的抽象,操作系统只认识内核线程。需要Go 调度器将多个 Goroutine 映射到实际的内核线程上。
对应会需要完成两项工作——Goroutine分发和创建线程。

Go 调度器的设计目标
- 使用少量的内核线程:创建内核线程代价高昂,因此调度器力求高效地复用内核线程。
- 支持高并发:Go 程序会涉及大量的 Goroutine,调度器需要满足这种需求。
- 利用并行性:原则上在N核机器上,Go程序应能够并行N个Goroutine。
何时进行调度?
操作可能影响 Goroutine 执行时,Go 调度器就会介入

- Goroutine 创建
- Goroutine 阻塞:如等待channel或mutex
- 系统调用: 阻塞的系统调用同时会阻塞内核线程
Go 调度器关键思想
- 运行队列(runqueue): 等待运行的 Goroutine 被放置在队列中。最初,使用的是单个全局队列,但在多线程情况下会导致竞争问题。解决方案是分布式运行队列,每个线程都获取自己的队列。
- 线程复用:空闲线程不进行销毁,会被用在检查全局运行队列、网络轮询、尝试运行gc任务、工作窃取上,如果仍然空闲会放置在一个链表中等待复用,以期最大利用并行性。
- GOMAXPROCS:限制正在运行Goroutine的线程数为CPU逻辑核心数。
- 工作窃取(work-stealing):如果一个线程的本地运行队列为空,它可以随机“窃取”其他线程队列中一半的任务,平衡线程之间的工作负载。
- 移交(hand-off): 当一个线程被阻塞(例如,由于系统调用)超过一段时间时,它关联的运行队列会由“sysmon”转交到另一个线程,此时唤醒(Unpark)可复用的线程,如果需要的话创建新线程(线程数限制只适用于正在运行Goroutine的线程),防止 Goroutine 饥饿。
- 协作式抢占: Go 调度时间节点依赖于程序自发调用调度器。但如果一个 Goroutine 长时间占用 CPU 不放,会由“sysmon”后台线程检测到(>10ms运行,警告)在可能的情况下抢占并将其放置于全局运行队列,确保公平性并防止饥饿。
- 全局运行队列: 全局运行队列充当被抢占的 Goroutine 的低优先级队列。
- “sysmon”线程:检测长时间运行的Goroutine,移交运行队列。
Go 调度过程示例
.gif?table=block&id=9201aa5d-7d46-4c3c-8e02-1c311b77b11b&t=9201aa5d-7d46-4c3c-8e02-1c311b77b11b&width=708&cache=v2)
局限性
- 没有 Goroutine 优先级: 所有 goroutine 都被平等对待,这在某些任务需要更高优先级的情况下可能会出现问题。
没有强抢占:协作式抢占无法保证公平性或延迟。
- 对硬件拓扑的感知有限: 调度器目前缺乏对底层硬件拓扑的感知,局部性利用程度不高。
参考资料与扩展阅读
- 作者:CamelliaV
- 链接:https://camelliav.netlify.app/article/gophercon
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。