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)和 goroutineG)的协调工作,确保了并发程序能够高效地执行和调度。调度器的设计和实现使得 Go 能够在大规模并发场景中表现优异。