<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GMP on Zampo Blog</title><link>https://blog.cpdd.fyi/tags/gmp/</link><description>Recent content in GMP on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 02 Jun 2026 17:36:48 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/tags/gmp/index.xml" rel="self" type="application/rss+xml"/><item><title>Go 为什么不怕 goroutine 死循环：抢占不是玄学，是一套停机协议</title><link>https://blog.cpdd.fyi/posts/go-gmp-preemption-gc/</link><pubDate>Tue, 02 Jun 2026 17:36:48 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gmp-preemption-gc/</guid><description>&lt;p&gt;把 &lt;code&gt;GOMAXPROCS&lt;/code&gt; 设成 1，再启动一个没有函数调用、没有 I/O、没有 sleep 的死循环。&lt;/p&gt;
&lt;p&gt;按很多人对 goroutine 的理解，程序应该完了：唯一的 P 被这个 goroutine 一直占着，其他 goroutine 没机会运行，GC 想停世界也停不下来。&lt;/p&gt;
&lt;p&gt;但 Go 1.14 之后，这个直觉已经不完整了。&lt;/p&gt;
&lt;p&gt;你可以跑一个很小的实验：一个 goroutine 进入 tight loop，另一个 goroutine 每 200ms 打印一次。默认运行时，ticker 还能继续打印；如果关掉异步抢占，程序可能在预期时间内完全没有输出。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; assets/go-gmp-series/03-preemption-gc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; go run examples/preemption-demo.go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;asyncpreemptoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; go run examples/preemption-demo.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;本机 Go 1.25.4 验证结果是：默认情况下 ticker 正常输出并退出；关闭 async preempt 后，2 秒内没有任何输出，命令超时。&lt;/p&gt;
&lt;p&gt;这件事不是“goroutine 很轻”能解释的。&lt;/p&gt;
&lt;p&gt;它真正说明的是：现代 Go 调度器不再完全依赖 goroutine 自觉让出 CPU。runtime 有能力发现某个 G 占 P 太久，然后用同步抢占或异步抢占，把它带回调度体系里。&lt;/p&gt;
&lt;p&gt;但这里最容易讲歪。&lt;/p&gt;
&lt;p&gt;Go 不是每 10ms 硬切一次 goroutine，也不是收到一个信号就能在任意机器指令上停住。抢占从来不是“想停就停”。它更像一份停机协议：runtime 要能停下这个 G，还要保证 GC 看得懂它的栈、寄存器和对象引用。&lt;/p&gt;</description></item><item><title>Go 调度器真正厉害的地方：线程卡住了，P 不能跟着陪葬</title><link>https://blog.cpdd.fyi/posts/go-gmp-syscall-netpoller/</link><pubDate>Fri, 29 May 2026 20:53:18 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gmp-syscall-netpoller/</guid><description>&lt;p&gt;把 &lt;code&gt;GOMAXPROCS&lt;/code&gt; 设成 1，再让一个 goroutine 卡在 &lt;code&gt;syscall.Read&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;按直觉，整个 Go 程序应该只剩一个执行名额。这个名额被卡住了，其他 goroutine 也该一起停。&lt;/p&gt;
&lt;p&gt;但你真跑一下，会看到另一件事：reader 卡在系统调用里，主 goroutine 的 ticker 还在继续打印。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; assets/go-gmp-series/02-syscall-netpoller/examples/syscall-handoff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;schedtrace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500,scheddetail&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; go run main.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;示例里一个 goroutine 进入阻塞的 &lt;code&gt;syscall.Read&lt;/code&gt;，3 秒后才有人往 pipe 写数据。与此同时，主 goroutine 每 200ms 仍然输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;main: still running Go code while another M may be blocked in syscall
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这件事比“goroutine 很轻”更关键。&lt;/p&gt;
&lt;p&gt;它说明 Go 调度器真正厉害的地方，不是能创建很多 G，而是能在一条 OS 线程卡住时，把 Go 世界继续往前推。&lt;/p&gt;
&lt;p&gt;第一篇我们讲过：G 是任务，M 是 OS 线程，P 是执行 Go 代码所必须持有的 runtime 资源。你可以把 P 类比成执行许可证。&lt;/p&gt;</description></item><item><title>Go GMP 里最容易背错的不是 G 和 M，而是 P</title><link>https://blog.cpdd.fyi/posts/go-gmp-scheduler-architecture/</link><pubDate>Sat, 23 May 2026 12:58:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/go-gmp-scheduler-architecture/</guid><description>&lt;p&gt;很多人聊 Go 调度器，开口就能背：G 是 goroutine，M 是 machine，P 是 processor。&lt;/p&gt;
&lt;p&gt;背完以后，问题来了：为什么已经有 M 这个 OS 线程，还要多一个 P？&lt;/p&gt;
&lt;p&gt;如果这个问题答不上来，GMP 其实还没真的理解。你只是记住了三个缩写，没有看懂 Go runtime 真正在拆哪几件事。&lt;/p&gt;
&lt;p&gt;P 最容易被讲错。有人把它说成 CPU 核心，有人把它说成调度器，有人干脆把它当成 G 和 M 之间的中间层。都不够准。&lt;/p&gt;
&lt;p&gt;更贴近源码的说法是：P 是执行 Go 代码所必须持有的 runtime 资源。&lt;/p&gt;
&lt;p&gt;它不是硬件 CPU 核心，而是 Go runtime 给 M 发的一张“执行许可证”。M 拿到 P，才能运行 Go 代码；M 没有 P，可以阻塞在 syscall 里，可以停着，可以等着，但不能继续执行普通 Go goroutine。&lt;/p&gt;
&lt;p&gt;这就是 GMP 设计里最关键的一刀。&lt;/p&gt;
&lt;p&gt;它把“任务”“线程”“执行资源”拆开了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/go-gmp-scheduler-architecture/cover.png" alt="Go GMP 调度器里的 P"&gt;&lt;/p&gt;
&lt;h2 id="g-不是线程是一份可被调度的任务"&gt;G 不是线程，是一份可被调度的任务&lt;/h2&gt;
&lt;p&gt;先看 G。&lt;/p&gt;
&lt;p&gt;G 很容易被叫成“轻量级线程”。这个说法方便，但也容易让人误会：好像 &lt;code&gt;go f()&lt;/code&gt; 就是开了一个便宜点的线程。&lt;/p&gt;
&lt;p&gt;实际不是。&lt;/p&gt;</description></item></channel></rss>