Go语言中的协作式的抢占式调度

发布于 1周前 作者 ionicwang 最后一次编辑是 5天前 来自 问答

Go语言中的协作式的抢占式调度

在 1.14 版本之前,程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度。

这种方式存在问题有: 某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿 垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分 钟的时间,导致整个程序无法工作

1 回复

在Go语言中,调度器(Scheduler)是Go运行时(runtime)的核心组件之一,它负责在多个goroutine之间分配CPU时间。Go的调度器是协作式(cooperative)与抢占式(preemptive)的混合体,但主要设计上是协作式的,直到Go 1.14版本引入了基于信号的抢占式调度来改进长时间运行或无限循环的goroutine的调度。

协作式调度

在协作式调度中,goroutine必须显式地让出CPU,通常是通过调用如runtime.Gosched()函数或进行I/O操作、系统调用等阻塞操作来实现。协作式调度的优点是简单且高效,但缺点是如果goroutine不合作(例如,长时间运行或无限循环),则可能导致其他goroutine饥饿。

抢占式调度

从Go 1.14开始,Go运行时引入了基于信号的抢占式调度机制,以解决协作式调度可能导致的问题。当goroutine执行时间过长时,Go运行时可能会发送一个信号给该goroutine所在的线程,迫使它暂停执行,并允许调度器将其他goroutine调度到该线程上执行。

示例代码

由于抢占式调度是由Go运行时自动管理的,因此没有直接的示例代码可以展示如何“触发”抢占式调度。但是,我们可以展示一个简单的goroutine示例,并说明在Go 1.14及以后版本中,长时间运行的goroutine如何可能被抢占:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func longRunningTask() {
    for i := 0; ; i++ {
        // 模拟长时间运行的任务
        time.Sleep(1 * time.Millisecond) // 假设这里是一个复杂的计算或循环
        if i%1000000 == 0 {
            fmt.Println("Running...", i)
        }
    }
}

func main() {
    // 设置GOMAXPROCS以控制并发执行的goroutine数量
    runtime.GOMAXPROCS(2)

    // 启动两个goroutine
    go longRunningTask()
    go longRunningTask()

    // 主goroutine等待,防止程序退出
    select {}
}

在上面的示例中,我们启动了两个执行长时间任务的goroutine。在Go 1.14及更高版本中,如果这两个goroutine中的任何一个执行时间过长,Go运行时可能会通过抢占式调度来中断它,以便其他goroutine有机会执行。

注意

  • 抢占式调度的具体实现细节(如何时触发抢占)是Go运行时内部的,对开发者是透明的。
  • 开发者通常不需要(也不应该)直接干预调度过程,除非在极端情况下需要优化性能。
  • 协作式调度仍然是Go调度器的基础,抢占式调度只是作为补充,用于处理极端情况。
回到顶部