Go 的调度器是实现高效并发的核心,它负责管理 goroutine
的调度、上下文切换和资源分配。为了详细了解 Go 调度器的工作原理,下面是对调度器内部实现的详细介绍,包括代码示例和解释。
1. Go 调度器的内部结构
1.1 主要组件
G
(Goroutine):代表一个goroutine
,包含了执行上下文的信息,比如栈、程序计数器等。M
(Machine):代表操作系统线程,用于实际执行goroutine
。P
(Processor):代表逻辑处理器,调度goroutine
的工作单位。每个P
维护一个运行队列,存放待执行的goroutine
。
1.2 数据结构
// Goroutine structure
type g struct {
stack stack // Goroutine's stack
status int // Goroutine's status (running, waiting, etc.)
sched bool // Indicates if the goroutine is scheduled
}
// Machine structure
type m struct {
g0 *g // Initial goroutine for the thread
curg *g // Currently running goroutine
p *p // Associated processor
// ... other fields
}
// Processor structure
type p struct {
runq queue // Queue for runnable goroutines
m *m // Associated OS thread
// ... other fields
}
// Queue for runnable goroutines
type queue struct {
head *g
tail *g
}
2. 调度器的工作原理
2.1 创建和销毁 goroutine
- 创建
goroutine
:
当调用 go
关键字启动新的 goroutine
时,Go 运行时系统会创建一个 G
对象并将其放入某个 P
的运行队列中。如果没有可用的 P
,则可能会将其放入系统队列中。
func newGoroutine(fn func()) *g {
g := &g{
stack: makeStack(),
status: runnable, // Mark as runnable
}
// Initialize the goroutine with the function to execute
g.stack = initStack(fn)
return g
}
func startGoroutine(g *g) {
p := getAvailableP() // Get an available processor
p.runq.push(g) // Add the goroutine to the processor's run queue
}
- 销毁
goroutine
:
当 goroutine
执行完毕或被取消时,调度器会清理相关资源。
func destroyGoroutine(g *g) {
// Clean up the goroutine's stack and other resources
freeStack(g.stack)
// Remove the goroutine from the run queue if necessary
}
2.2 调度和上下文切换
- 调度:
调度器周期性地从每个 P
的运行队列中取出一个 goroutine
,将其分配给空闲的 M
线程执行。
func schedule(p *p) {
g := p.runq.pop() // Get the next goroutine from the run queue
if g != nil {
p.m.curg = g // Assign the goroutine to the OS thread
executeGoroutine(g)
}
}
- 上下文切换:
goroutine
的上下文切换涉及到保存和恢复 goroutine
的寄存器和栈。
func contextSwitch(g1 *g, g2 *g) {
saveContext(g1) // Save the context of the current goroutine
loadContext(g2) // Load the context of the new goroutine
}
2.3 抢占式调度
- 抢占:
调度器会定期对正在运行的 goroutine
进行抢占,以防止某个 goroutine
长时间占用 CPU。
func checkPreemption() {
current := getCurrentGoroutine()
if needsPreemption(current) {
// Preempt the current goroutine and reschedule it
rescheduleGoroutine(current)
}
}
- 调度周期:
调度器设置了一个调度周期(通常是几毫秒),在每个周期结束时,调度器会检查当前正在运行的 goroutine
并决定是否需要进行上下文切换。
func schedulerTick() {
for {
time.Sleep(schedulingInterval) // Wait for the scheduling interval
checkPreemption() // Check if preemption is needed
}
}
2.4 处理阻塞和等待
- 阻塞:
当 goroutine
在等待某些资源或操作时(例如等待 channel
发送或接收),调度器会将其从运行队列中移除。
func blockGoroutine(g *g) {
g.status = blocked // Mark the goroutine as blocked
// Move the goroutine to the waiting queue
moveToWaitingQueue(g)
}
- 等待队列:
阻塞的 goroutine
会被放入等待队列中。调度器会在资源变得可用时,检查等待队列并重新调度这些 goroutine
。
func unblockGoroutine(g *g) {
if isResourceAvailable() {
g.status = runnable
p := getAvailableP()
p.runq.push(g) // Add the goroutine back to the run queue
}
}
2.5 工作窃取
- 工作窃取:
调度器使用工作窃取算法来平衡负载。一个忙碌的 P
可以从其他较空闲的 P
中窃取 goroutine
。
func stealWork(fromP *p, toP *p) {
if !fromP.runq.isEmpty() {
g := fromP.runq.pop()
toP.runq.push(g) // Move the goroutine to the other processor's queue
}
}
3. 调度器的优化和调整
3.1 GOMAXPROCS
- GOMAXPROCS:
通过 runtime.GOMAXPROCS
函数或环境变量设置可以调整并发的线程数。GOMAXPROCS
控制了同时运行的操作系统线程的数量,从而影响 goroutine
的调度和性能。
runtime.GOMAXPROCS(4) // Set the number of OS threads to 4
3.2 调度器性能
- 调度器优化:
Go 的调度器经过多年的优化,能够高效地处理大量 goroutine
。它通过减少上下文切换的开销、优化调度算法和改进抢占机制来提高性能。
总结
Go 的调度器通过 M:N 模型、轻量级上下文切换、抢占式调度和工作窃取算法实现了高效的并发管理。它利用操作系统线程(M
)、逻辑处理器(P
)和 goroutine
(G
)的协调工作,确保了并发程序能够高效地执行和调度。调度器的设计和实现使得 Go 能够在大规模并发场景中表现优异。