从runtime源码彻底搞懂Go调度器GMP模型"

admin 2026-10-18 19:10:37 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

从 runtime 源码彻底搞懂 Go 调度器 GMP 模型

原创

go
go

Go语言教程

2026年7月1日 13:13
陕西

在小说阅读器读本章

去阅读

go func(){} 这一行,看着轻,其实 runtime 后面马上就开始搬东西了。

别急着背 G、M、P 那三句话。真看源码,先从 runtime/proc.go 开始。文件开头已经把话说得很直:调度器的活,就是把 ready-to-run 的 goroutine 分发到 worker thread 上;G 是 goroutine,M 是 worker thread,P 是执行 Go 代码必须拿到的资源。M 没有 P,可以阻塞在 syscall 里,但不能跑用户态 Go 代码。

我第一次看 GMP,卡住的地方不是概念,是这个问题:

为什么有了线程 M,还非得多一个 P?

后来看到 runtime/HACKING 里那段解释就顺了。P 里面放的是执行 Go 代码需要的资源,比如调度器状态、内存分配状态,而且 P 的数量就是 GOMAXPROCS。调度器要做的,是把一个 G、一个 M、一个 P 凑在一起。M 进 syscall 了,P 还能交出去,让别的 M 接着跑 Go 代码。

这地方别想得太玄。

G 是活。 M 是干活的线程。 P 是工位。

没有工位,线程站着也干不了 Go 代码。

看一段小代码,先造一点调度痕迹:

package main

import (
 \"fmt\"
 \"runtime\"
 \"sync\"
 \"time\"
)

func main() {
 runtime.GOMAXPROCS(2)

 var wg sync.WaitGroup
 for i := 0; i < 8; i++ {
  jobID := i
  wg.Add(1)

  go func() {
   defer wg.Done()

   begin := time.Now()
   sum := 0
   for n := 0; n < 20_000_000; n++ {
    sum += n ^ jobID
   }

   fmt.Printf(\"job=%d cost=%s sum=%d\
\", jobID, time.Since(begin), sum)
  }()
 }

 wg.Wait()
}

这里开了 8 个 G,但 GOMAXPROCS(2) 只有 2 个 P。也就是说,同一时刻真正能跑 Go 代码的,最多就两个“工位”。剩下的 G,不是没线程,是还没排到 P。

再往源码里看,go 语句会被编译器变成对 runtime.newproc 的调用。newproc 里会创建新的 G,然后把它丢进当前 P 的运行队列,最后 wakep() 试着唤醒一个 P 去干活。这个链路在 proc.go 里很清楚:newproc -> newproc1 -> runqput -> wakep。

这里我一般重点看 runqput。

它不是傻乎乎全塞一个全局队列。它会优先把 G 放到当前 P 的本地队列;如果 next=true,还会尝试放到 pp.runnext,让这个 G 更快被执行;本地队列满了,才会走慢路径,把一批任务甩到全局队列。

这就是 Go 调度器快的一个点:少抢全局锁,尽量让每个 P 自己消化自己的活。

那 M 怎么拿 G?

看 schedule()。

源码注释写得很硬:一轮调度,就是找一个可运行的 goroutine,然后执行它。schedule() 里最关键的一句是:

gp, inheritTime, tryWakeP := findRunnable()

找到之后再 execute(gp, inheritTime)。execute 会把 G 状态从 _Grunnable 切到 _Grunning,绑定到当前 M 上,然后跳到 G 的执行现场。

真正脏活在 findRunnable()。

它的注释比很多文章都靠谱:尝试从本地队列、全局队列、网络轮询器里拿 G,还会从其他 P 那里偷任务。

所以调度顺序大概是这种味道:

先看自己 P 本地有没有活; 没有,再看全局队列; 还没有,看 netpoll 有没有网络事件醒来的 G; 再没有,就去别的 P 队列里偷一半; 实在没活,M 该停就停,别空转烧 CPU。

偷任务这一步很关键。假设 P0 队列爆了,P1 空着,P1 对应的 M 不会在那里装死,它会去 P0 那边偷一批 G 回来跑。这个设计很工程,不浪漫,但很顶用。

再写个更接近线上排查的例子。很多人一看 goroutine 数高,就说“调度器不行”。我一般先打两个数:

package main

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

func main() {
 for i := 0; i < 5000; i++ {
  go func(id int) {
   time.Sleep(5 * time.Second)
   if id == -1 {
    fmt.Println(\"never\")
   }
  }(i)
 }

 ticker := time.NewTicker(time.Second)
 defer ticker.Stop()

 for range ticker.C {
  fmt.Printf(\"goroutines=%d gomaxprocs=%d\
\",
   runtime.NumGoroutine(),
   runtime.GOMAXPROCS(0),
  )
 }
}

这段代码 goroutine 很多,但大部分在 sleep,没怎么抢 P。线上如果看到 goroutine 数高,别急着扩机器,先分清楚它们是在 runnable,还是 waiting。前者才真会挤调度器,后者更多要查锁、channel、IO、定时器。

要看得更实一点,用 trace:

package main

import (
 \"os\"
 \"runtime/trace\"
 \"sync\"
)

func main() {
 f, err := os.Create(\"gmp.trace\")
 if err != nil {
  panic(err)
 }
 defer f.Close()

 if err := trace.Start(f); err != nil {
  panic(err)
 }
 defer trace.Stop()

 var wg sync.WaitGroup
 for i := 0; i < 16; i++ {
  wg.Add(1)
  go func(seed int) {
   defer wg.Done()
   x := seed
   for n := 0; n < 30_000_000; n++ {
    x = x*1664525 + 1013904223
   }
  }(i)
 }
 wg.Wait()
}

跑完:

go run main.go
go tool trace gmp.trace

Go 官方也建议用 execution trace 看调度动作,里面能看到 goroutine 调度、P 空转、GC 时间等信息,排延迟问题比只看日志靠谱。

最后把 GMP 串一下。

go 语句创建 G,先放到 P 的本地队列。本地队列放不下,就挪一部分到全局队列。M 必须拿到 P 才能跑 Go 代码。schedule() 不停找可运行的 G,findRunnable() 找不到就偷,偷不到就看 netpoll,再没活就让 M 停下来。G 被选中后,execute() 把它切成 running,真正开始跑。

这套东西看着绕,本质不是为了炫技,是为了几个很现实的目标:少创建线程,少抢全局锁,尽量利用 CPU,IO 或 syscall 阻塞时别把 P 一起拖死。

所以排 Go 程序性能问题时,我不太信“goroutine 多所以慢”这种粗暴判断。先看 runnable 堆不堆,再看 P 有没有空,再看 syscalls、netpoll、GC 有没有抢时间。GMP 不是背出来的,是你顺着 newproc、runqput、schedule、findRunnable、execute 这几根线摸一遍,才真能用上。


    从runtime源码彻底搞懂Go调度器GMP模型" 网络安全文章

    从runtime源码彻底搞懂Go调度器GMP模型"

    文章基于Go源码解析GMP调度机制,指出G为任务、M为线程、P为执行工位。G优先入本地队列以减少锁竞争,M按本地、全局、netpoll及偷取顺序获取任务。性能排查切忌盲目归咎高协程数,需区分runnable与waiting状态,建议用runtime/trace精准定位瓶颈。
    当AI幻觉照进现实:一种从未见过的浏览器勒索攻击正在诞生" 网络安全文章

    当AI幻觉照进现实:一种从未见过的浏览器勒索攻击正在诞生"

    该文档揭示了一种新型浏览器勒索攻击技术,利用AI生成的代码结合FileSystemAccessAPI,在用户授权后加密本地文件。攻击者通过伪装成AI图片处理工具诱导用户授权文件夹访问权限,特别在Android设备上风险更高,可加密相册内容。研究显示AI降低了攻击创新门槛,使理论风险转化为可行方案。建议用户谨慎授权浏览器文件夹访问,保持备份并警惕AI主题工具。
    评论:0   参与:  0