<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 9 章 goroutine 调度器 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/</link><description>Recent content in 第 9 章 goroutine 调度器 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/index.xml" rel="self" type="application/rss+xml"/><item><title>9.1 调度问题与 GMP 模型</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/model/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/model/</guid><description>&lt;h1 id="91-调度问题与-gmp-模型"&gt;9.1 调度问题与 GMP 模型&lt;/h1&gt;
&lt;p&gt;写下 &lt;code&gt;go f()&lt;/code&gt;，一个 goroutine 就开始运行了。这行代码背后，是 Go 运行时最精巧的一台机器：
调度器。它要回答一个并不简单的问题：成千上万个 goroutine，如何在为数不多的几个 CPU 核心上
轮转，既跑得快，又让用户几乎察觉不到它的存在。本节先把它要解决的问题、整体骨架，以及它在
并发运行时这个大家族里的位置交代清楚，后面各节再深入每个部件。&lt;/p&gt;
&lt;h2 id="911-三种线程模型与一段历史"&gt;9.1.1 三种线程模型，与一段历史&lt;/h2&gt;
&lt;p&gt;把&amp;quot;并发任务&amp;quot;映射到&amp;quot;CPU 执行资源&amp;quot;上，历史上有三种模型，它们的取舍决定了一切。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1:1（内核线程）&lt;/strong&gt;：每个用户线程对应一个操作系统线程。真并行、阻塞系统调用由内核透明
处理、实现简单；代价是每次创建与切换都要陷入内核，每个线程还要预留以兆字节计的栈。
Linux 的 NPTL、现代 Windows、Java 的平台线程都走这条路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N:1（纯用户线程，即早期的「绿色线程」）&lt;/strong&gt;：多个用户线程挤在一个操作系统线程上。切换
极廉价、栈极小；但&lt;strong&gt;用不上多核&lt;/strong&gt;，而且&lt;strong&gt;一次阻塞的系统调用会卡住全部线程&lt;/strong&gt;,这是它的死穴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M:N（混合 / 两级）&lt;/strong&gt;：把 $M$ 个用户线程多路复用到 $N$ 个内核线程上。既廉价又能并行，
代价是需要&lt;strong&gt;用户态与内核态两个调度器协作&lt;/strong&gt;,这正是复杂度的根源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;历史在这里拐过一个有意思的弯。1990 年代，M:N 一度被寄予厚望，最有影响的方案是
Anderson 等人的&lt;strong&gt;调度器激活&lt;/strong&gt;（scheduler activations，SOSP 1991）：让内核在阻塞、就绪等
时刻&amp;quot;通知&amp;quot;用户态调度器，使两个调度器协同。但工业界最终大多退回了 1:1。Drepper 与 Molnar
为 Linux 设计 NPTL 时（2005）把理由写得很直白：M:N 需要两个调度器，若不协同则性能受损，
而要让它们协同所需引入内核基础设施的成本与维护代价都太高，&amp;ldquo;不符合 Linux 内核的理念&amp;rdquo;。
于是 Linux 选了 1:1。&lt;/p&gt;

&lt;script src="http://golang.design/under-the-hood/mermaid.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
 window.addEventListener("DOMContentLoaded", function () {
 mermaid.initialize({
 startOnLoad: false,
 theme: "neutral",
 securityLevel: "strict",
 fontFamily: "inherit",
 });
 mermaid.run({ querySelector: ".mermaid" });
 });
&lt;/script&gt;


&lt;pre class="mermaid"&gt;flowchart LR
 subgraph OS[操作系统]
 M1[M1 线程]
 M2[M2 线程]
 end
 subgraph RT[Go 运行时]
 direction TB
 P1[&amp;#34;P1 · 本地队列 G G G&amp;#34;]
 P2[&amp;#34;P2 · 本地队列 G G&amp;#34;]
 GQ[&amp;#34;全局队列 G G G G&amp;#34;]
 end
 M1 --&amp;gt; P1
 M2 --&amp;gt; P2
 P2 -. 空闲时窃取一半 .-&amp;gt; P1
 P1 -. 队列溢出 .-&amp;gt; GQ
 GQ -. 定期补充 .-&amp;gt; P2&lt;/pre&gt;
&lt;p&gt;Go 偏偏又走回了 M:N。它之所以能避开当年的坑，靠的是三件事，而这三件事都握在运行时自己手里：
goroutine 的栈很小且可增长（起步几 KB），创建与切换都廉价；运行时掌握所有会阻塞的点，
能在阻塞前把执行权让出；网络 I/O 由内置的网络轮询器（&lt;a href=".././poller"&gt;9.9&lt;/a&gt;）接管，阻塞的
goroutine 会被挂起而不占用线程。当年杀死 N:1 的&amp;quot;阻塞系统调用&amp;quot;难题，Go 是在运行时内部解决的，
而不是去求内核提供激活机制。&lt;/p&gt;</description></item><item><title>9.2 工作窃取式调度</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/steal/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/steal/</guid><description>&lt;h1 id="92-工作窃取式调度"&gt;9.2 工作窃取式调度&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././model"&gt;9.1&lt;/a&gt; 留下一个问题：每个 P 各有一条本地队列，活儿难免分布不均，有的 P 忙不过来，
有的 P 无所事事。如何在不引入中心瓶颈的前提下把负载摊匀，是并发调度的核心难题。Go 的答案，
是一个有着三十年理论积累、并在整个工业界反复出现的设计：工作窃取（work stealing）。&lt;/p&gt;
&lt;p&gt;本节会比别处走得更深一些：先讲清 Go 怎么做，再追到它背后的调度理论（为什么它「可证明地好」），
然后横看它在 Cilk、Java、Rust 等系统里的不同化身，最后停在仍然开放的问题上。&lt;/p&gt;
&lt;h2 id="921-共享还是窃取"&gt;9.2.1 共享还是窃取&lt;/h2&gt;
&lt;p&gt;把任务在处理器间挪动，历史上有两种范式。&lt;strong&gt;工作共享&lt;/strong&gt;（work sharing）：谁生出新任务，就主动把
一部分推给空闲的处理器。&lt;strong&gt;工作窃取&lt;/strong&gt;（work stealing）：空闲的处理器自己动手，去别人那里把任务
偷过来。差别在迁移的频率。工作共享只要有新任务就可能触发迁移；工作窃取只在某个处理器真的
没活干时才迁移，当所有处理器都忙，窃取者找不到下手的机会，迁移自然停止。&lt;strong&gt;负载越重，
工作窃取反而越安静&lt;/strong&gt;，这是它相对工作共享的根本优势，也有严格的通信量界限支撑（见 9.2.4）。&lt;/p&gt;
&lt;h2 id="922-go-的找活儿顺序"&gt;9.2.2 Go 的找活儿顺序&lt;/h2&gt;
&lt;p&gt;一个绑定了 P 的 M 运行完手头的 G 后，并不直接去窃取，而是按一条由近及远、由廉价到昂贵的
顺序搜索（运行时的 &lt;code&gt;findRunnable&lt;/code&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// findRunnable：M 找下一个可运行的 G（伪代码）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; findRunnable() *g {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; pp.schedtick%61 == 0 &amp;amp;&amp;amp; !sched.runq.empty() { &lt;span style="color:#008000"&gt;// 1. 每 61 次先看全局，保证公平&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; gp := globrunqget(); gp != &lt;span style="color:#00f"&gt;nil&lt;/span&gt; { &lt;span style="color:#00f"&gt;return&lt;/span&gt; gp }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; gp := runqget(pp); gp != &lt;span style="color:#00f"&gt;nil&lt;/span&gt; { &lt;span style="color:#00f"&gt;return&lt;/span&gt; gp } &lt;span style="color:#008000"&gt;// 2. 本地队列（含 runnext）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; gp := globrunqget(); gp != &lt;span style="color:#00f"&gt;nil&lt;/span&gt; { &lt;span style="color:#00f"&gt;return&lt;/span&gt; gp } &lt;span style="color:#008000"&gt;// 3. 全局队列&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; gp := netpoll(); gp != &lt;span style="color:#00f"&gt;nil&lt;/span&gt; { &lt;span style="color:#00f"&gt;return&lt;/span&gt; gp } &lt;span style="color:#008000"&gt;// 4. 网络轮询器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; gp := stealWork(); gp != &lt;span style="color:#00f"&gt;nil&lt;/span&gt; { &lt;span style="color:#00f"&gt;return&lt;/span&gt; gp } &lt;span style="color:#008000"&gt;// 5. 从其他 P 窃取一半&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stopm() &lt;span style="color:#008000"&gt;// 6. 实在没有：自旋或休眠&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&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;三个让窃取高效的细节。&lt;strong&gt;本地队列有界&lt;/strong&gt;：每个 P 是一个定长 256 的环形缓冲，绝大多数入队出队
无锁；放满时把一半搬到全局队列（&lt;code&gt;runqputslow&lt;/code&gt;）兜底。&lt;strong&gt;窃取目标随机且打散&lt;/strong&gt;：若所有空闲 P 都
从同一起点按固定顺序去偷，会一窝蜂挤向同一目标。Go 让每个窃取者以随机起点加上一个与 P 总数
&lt;strong&gt;互质&lt;/strong&gt;的随机步长，走出覆盖全部 P 的伪随机排列，互质保证不重不漏，随机化避免羊群效应。
&lt;strong&gt;自旋线程&lt;/strong&gt;：允许少量 M 处于自旋态（上限 &lt;code&gt;GOMAXPROCS&lt;/code&gt;，计于 &lt;code&gt;sched.nmspinning&lt;/code&gt;）主动找活儿
而不立刻休眠，新就绪的 G 能被迅速接住，免去频繁的线程休眠唤醒。&lt;/p&gt;</description></item><item><title>9.3 MPG 模型与并发调度单元</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/mpg/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/mpg/</guid><description>&lt;h1 id="93-mpg-模型与并发调度单元"&gt;9.3 MPG 模型与并发调度单元&lt;/h1&gt;
&lt;p&gt;调度器要回答的第一个问题不是「怎么调度」，而是「调度什么」。Go 把这个被调度的对象叫
goroutine，并用一套 M、P、G 三元组承载它。在动手分析调度算法（&lt;a href=".././exec"&gt;9.4&lt;/a&gt; 起）之前，
本节先把这三个调度单元安顿好：goroutine 在计算机科学谱系里究竟是什么、它的运行现场如何编码、
为何调度本身要在一个特殊的 g0 上进行、一个 goroutine 的一生会经历哪些状态，以及承载它们的
工作线程 M 如何被暂止与复始。读懂这几样东西，后面的调度算法就只是「在这些单元之间搬运 G」。&lt;/p&gt;
&lt;p&gt;为避免落回逐字段翻译源码的窠臼，下文给出的结构体都是裁剪后的速写：只保留与设计相关的字段，
并在注释里说明它为何存在。完整定义可对照 &lt;code&gt;runtime/runtime2.go&lt;/code&gt; 与 &lt;code&gt;runtime/proc.go&lt;/code&gt;（本节对标
go1.26）。&lt;/p&gt;
&lt;h2 id="931-goroutine-是什么有栈协程"&gt;9.3.1 goroutine 是什么：有栈协程&lt;/h2&gt;
&lt;p&gt;goroutine 常被一句「轻量级线程」带过，这话不错，却遮住了它真正的身世。把它放回计算机科学的
谱系里，goroutine 是一个&lt;strong&gt;有栈协程&lt;/strong&gt;（stackful coroutine）。&lt;/p&gt;
&lt;p&gt;协程的概念可上溯到 Conway 1963 年提出 coroutine 一词时的设想：两段子程序互为对方的调用者，
能在中途交出控制权、又能从交出处原样恢复，而非像普通函数那样必须执行到底才返回。Moura 与
Ierusalimschy 在 2009 年给协程做了一份清晰的分类，两条正交的轴至今仍是讨论协程的基本坐标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对称（symmetric）与非对称（asymmetric）&lt;/strong&gt;：对称协程之间地位平等，靠一个统一的 transfer
原语彼此跳转；非对称协程则有明确的「调用者」与「被调者」，被调者只能让出（yield）回它的
调用者。Go 的用户看不到 yield，但运行时内部 goroutine 与调度循环之间正是非对称的让出关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有栈（stackful）与无栈（stackless）&lt;/strong&gt;：有栈协程拥有自己独立的调用栈，因此可以在&lt;strong&gt;任意
深度的嵌套调用中&lt;/strong&gt;挂起，挂起点不必是协程入口函数本身；无栈协程则没有独立栈，只能在顶层
函数里挂起，深层调用想挂起就得把整条调用链改写成状态机。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;goroutine 落在「非对称、有栈」这一格。有栈这一点尤其关键，它意味着一个 goroutine 可以在
任意函数、任意调用深度处被挂起（无论是主动 &lt;code&gt;&amp;lt;-ch&lt;/code&gt; 阻塞，还是被调度器抢占），挂起时整条 Go
调用栈连同其上的局部变量原封不动地保留，复始时从断点继续。用更理论的话说，一次挂起就是对
当前执行的一次&lt;strong&gt;一次性定界续延&lt;/strong&gt;（one-shot delimited continuation）的捕获，而这份续延的物理
形态，就是下一节要讲的 &lt;code&gt;gobuf&lt;/code&gt;：一组保存下来的寄存器（SP、PC 等），足以让执行从断点恢复。&lt;/p&gt;</description></item><item><title>9.4 调度循环</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/schedule/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/schedule/</guid><description>&lt;h1 id="94-调度循环"&gt;9.4 调度循环&lt;/h1&gt;
&lt;p&gt;前面几节备齐了材料：知道了 G、M、P 是什么（&lt;a href=".././mpg"&gt;9.3&lt;/a&gt;），知道了一个 M 怎样找活儿
（&lt;a href=".././steal"&gt;9.2&lt;/a&gt;）。这一节把它们真正转起来，看调度循环如何在一个线程上一刻不停地挑选并
运行 goroutine，以及它如何在「让单个 goroutine 跑得久一点」（吞吐与局部性）与「别让任何
goroutine 饿死」（公平）之间拿捏分寸。&lt;/p&gt;
&lt;p&gt;下文的代码一律是&lt;strong&gt;裁剪后的速写&lt;/strong&gt;，只保留与设计相关的骨架，去掉了 GC、tracing、profiling、
锁定线程等旁支。完整定义可对照 &lt;code&gt;runtime/proc.go&lt;/code&gt;，下文涉及的版本均为 go1.26。&lt;/p&gt;
&lt;h2 id="941-一个永不返回的循环"&gt;9.4.1 一个永不返回的循环&lt;/h2&gt;
&lt;p&gt;Go 的调度是&lt;strong&gt;协作式、运行到让出&lt;/strong&gt;（run-to-yield）的：一个 goroutine 一旦被选中，就一直跑到它
主动让出、阻塞、或被抢占（&lt;a href=".././preemption"&gt;9.7&lt;/a&gt;）为止，而不是像内核那样被时钟中断按固定
时间片切走。每个工作线程从 &lt;code&gt;mstart&lt;/code&gt; 启动后，最终进入调度循环 &lt;code&gt;schedule&lt;/code&gt;，此后在其中周而复始，
直到线程退出。骨架剥到最简，就是一个两步循环：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 每个 M 的调度循环（速写）：运行在系统栈 g0 上，永不返回&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; schedule() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 找一个可运行的 G（见 9.2 的完整顺序）；找不到就阻塞在 findRunnable 内，直到有活儿&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gp, inheritTime, _ := findRunnable()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 切到 gp 的栈开始执行。控制权要再回到这里，靠的是下面的 mcall，而非函数返回&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; execute(gp, inheritTime)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&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;&lt;code&gt;findRunnable&lt;/code&gt; 永不返回 &lt;code&gt;nil&lt;/code&gt;：取不到活儿，它会让 M 转入自旋或休眠，阻塞在内部直到被唤醒，
因此 &lt;code&gt;schedule&lt;/code&gt; 不必处理「无事可做」的分支。而 &lt;code&gt;execute&lt;/code&gt; 也永不返回，它跳进用户 G 的栈，
此后 &lt;code&gt;schedule&lt;/code&gt; 这一帧的栈空间就被复用了。控制权要回到调度逻辑，靠的是 9.4.2 的栈切换。&lt;/p&gt;</description></item><item><title>9.5 线程管理</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/thread/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/thread/</guid><description>&lt;h1 id="95-线程管理"&gt;9.5 线程管理&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././model"&gt;9.1&lt;/a&gt; 立下 GMP 的三层结构：G 是用户态的执行单元，P 是调度的许可证与本地资源，
M 才是真正向操作系统借来的那条腿。前几节谈 G 与 P 居多，这一节把目光落到 M 上，回答几个被
一直搁置的问题：M 到底是什么、它从哪里来、为什么 GOMAXPROCS 限的是 P 而线程数常常多于它、
一次阻塞的系统调用为何不会把别的 G 一同拖死，以及用户想把一个 Goroutine 钉死在某条线程上时
（&lt;code&gt;LockOSThread&lt;/code&gt;），运行时要为此付出什么代价。&lt;/p&gt;
&lt;p&gt;贯穿全节的一个判断是：&lt;strong&gt;线程是昂贵的资源&lt;/strong&gt;。创建它要陷入内核、要分配栈、要登记信号掩码；
销毁它同样不便宜。Go 调度器的许多设计，从复用空闲 M、到把系统调用中的 P 交接出去、再到给
线程数量设一道一万的保险丝，都是围绕「尽量少创建、尽量多复用」这一条主线展开的。&lt;/p&gt;
&lt;h2 id="951-m-即操作系统线程"&gt;9.5.1 M 即操作系统线程&lt;/h2&gt;
&lt;p&gt;M（machine）是对一条操作系统线程的抽象。进程启动时，引导线程被包装成 &lt;code&gt;m0&lt;/code&gt;，它是全局变量，
随进程一同存在，不经堆分配；此后每一个 M 都对应一条由运行时显式创建的内核线程。M 与 G 的关系
是「线程跑 Goroutine」：M 持有一个 P 后，从 P 的本地队列里取 G 来执行。裁剪后的速写只看与
线程管理相关的字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// m：一条操作系统线程的运行时抽象（速写）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; m &lt;span style="color:#00f"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; g0 *g &lt;span style="color:#008000"&gt;// 调度用的系统栈 goroutine：跑调度器代码、处理信号&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curg *g &lt;span style="color:#008000"&gt;// 当前正在此 M 上执行的用户 goroutine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; p puintptr &lt;span style="color:#008000"&gt;// 当前持有的 P；进入系统调用时可能被剥离&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nextp puintptr &lt;span style="color:#008000"&gt;// 被唤醒后将要绑定的 P（stopm 醒来时用）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; oldp puintptr &lt;span style="color:#008000"&gt;// 进入系统调用前持有的 P，留待 exitsyscall 快速取回&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; park note &lt;span style="color:#008000"&gt;// 线程在此「信号量」上睡眠/唤醒，复用 M 的核心机制&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schedlink muintptr &lt;span style="color:#008000"&gt;// 串入空闲 M 链表 / newmHandoff 链表&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lockedg guintptr &lt;span style="color:#008000"&gt;// 与某个 G 互锁（LockOSThread），见 9.5.6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lockedExt &lt;span style="color:#2b91af"&gt;int32&lt;/span&gt; &lt;span style="color:#008000"&gt;// 外部（用户）锁定计数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lockedInt &lt;span style="color:#2b91af"&gt;int32&lt;/span&gt; &lt;span style="color:#008000"&gt;// 内部（运行时）锁定计数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; incgo &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt; &lt;span style="color:#008000"&gt;// 是否正执行 cgo 调用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; isextra &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt; &lt;span style="color:#008000"&gt;// 是否为 cgo 回调而生的 extra-M，见 9.5.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&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;新线程经由 &lt;code&gt;newm&lt;/code&gt; → &lt;code&gt;newm1&lt;/code&gt; → &lt;code&gt;newosproc&lt;/code&gt; 创建。在 Linux 上，&lt;code&gt;newosproc&lt;/code&gt; 最终落到一次
&lt;code&gt;clone(2)&lt;/code&gt; 系统调用，所用的标志位说明了「线程」与「进程」的分野：&lt;/p&gt;</description></item><item><title>9.6 信号处理机制</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/signal/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/signal/</guid><description>&lt;h1 id="96-信号处理机制"&gt;9.6 信号处理机制&lt;/h1&gt;
&lt;p&gt;操作系统信号是异步的、底层的：它可能在任意时刻打断任意线程，而处理器（signal handler）里
能做的事又极其受限。Go 用户想要的，却往往是用 &lt;code&gt;signal.Notify&lt;/code&gt; 把一个 channel 接上 &lt;code&gt;SIGINT&lt;/code&gt;，
在收到时优雅收尾。运行时要做的，正是在这两者之间搭一座桥：把凶险的异步信号，转化成可被
goroutine 安然消费的事件。这座桥的每一处设计，都受「信号上下文里能做什么」这一硬约束支配。
读懂了这条约束，本节其余的机制就都只是它的推论。&lt;/p&gt;
&lt;h2 id="961-异步信号安全处理器里几乎什么都不能做"&gt;9.6.1 异步信号安全：处理器里几乎什么都不能做&lt;/h2&gt;
&lt;p&gt;信号处理器运行在一个被打断的、不确定的上下文里。被打断的线程此刻可能正持有 &lt;code&gt;malloc&lt;/code&gt; 的锁、
正处在某个数据结构的中间状态，甚至正在分配器内部改写元数据。一旦处理器里再去调用
&lt;code&gt;malloc&lt;/code&gt; 或获取同一把锁，便会自我死锁，或读到半成品状态而崩溃。POSIX 因此规定，信号处理器
里只能调用&lt;strong&gt;异步信号安全&lt;/strong&gt;（async-signal-safe）的函数，这是一份很短的白名单（见
&lt;code&gt;signal-safety(7)&lt;/code&gt;）：&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;_exit&lt;/code&gt;、&lt;code&gt;sigaction&lt;/code&gt; 这类不碰锁、不碰分配器的系统调用在列，
而 &lt;code&gt;malloc&lt;/code&gt;、&lt;code&gt;printf&lt;/code&gt;、绝大多数 &lt;code&gt;pthread&lt;/code&gt; 加锁、乃至大部分 C 标准库都&lt;strong&gt;不在&lt;/strong&gt;其列。&lt;/p&gt;
&lt;p&gt;这条约束逼出了所有支持信号的运行时共同遵守的一条铁律：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在处理器里只做不得不做的最少事，把真正的处理推迟到一个安全的地方去做。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这正是经典手法&lt;strong&gt;自管道技巧&lt;/strong&gt;（self-pipe trick，W. R. Stevens 在 APUE 中给出的范式）的来由：
处理器什么都不干，只往一个事先建好的管道写一个字节；主事件循环（&lt;code&gt;select&lt;/code&gt;/&lt;code&gt;poll&lt;/code&gt;）在管道另一头
监听，于是「收到信号」被降格成一次普通的「管道可读」事件，后续处理便回到了不受限的常规上下文里。
Linux 后来用 &lt;code&gt;signalfd(2)&lt;/code&gt; 把这一手法收编进内核，让信号能直接以文件描述符的形式被 &lt;code&gt;epoll&lt;/code&gt; 消费。&lt;/p&gt;
&lt;p&gt;Go 走的是同一思路的变体，只是把那根「管道」换成了运行时内部的一个&lt;strong&gt;无锁队列&lt;/strong&gt;（&lt;a href="#964-sigsend%E6%97%A0%E9%94%81%E9%98%9F%E5%88%97%E4%B8%8E%E4%B8%93%E8%81%8C%E6%8E%A5%E6%94%B6-goroutine"&gt;9.6.4&lt;/a&gt;）。
理由也很 Go：自管道每次投递都要一次 &lt;code&gt;write&lt;/code&gt; 系统调用，而 Go 的信号处理与调度器、垃圾回收
深度耦合，用一个进程内的原子状态机来「写一个 bit、唤醒一个等待者」，比反复进出内核更省，也更
可控。本节接下来的几小节，便是沿着这条铁律，看 Go 如何把它落实到处理器、备用栈、队列与
专职 goroutine 这几样零件上。&lt;/p&gt;
&lt;h2 id="962-gsignal处理器自带的一段安全栈"&gt;9.6.2 gsignal：处理器自带的一段安全栈&lt;/h2&gt;
&lt;p&gt;铁律的第一处落实，是给处理器一段&lt;strong&gt;专门的栈&lt;/strong&gt;。信号可能在用户 goroutine 的栈快用尽时降临，
若处理器还在这段紧绷的栈上运行，极易触发栈溢出；更麻烦的是，Go 的栈是可增长的（&lt;a href="../../part4memory/ch14stack"&gt;14.6&lt;/a&gt;），
而栈增长本身要分配、要加锁，正是处理器里碰不得的操作。解法是 POSIX 的 &lt;code&gt;sigaltstack(2)&lt;/code&gt;：
为线程预先登记一段&lt;strong&gt;备用信号栈&lt;/strong&gt;，内核在分发信号时自动切到这段栈上执行处理器。&lt;/p&gt;</description></item><item><title>9.7 协作与抢占</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/preemption/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/preemption/</guid><description>&lt;h1 id="97-协作与抢占"&gt;9.7 协作与抢占&lt;/h1&gt;
&lt;p&gt;我们在 &lt;a href=".././schedule"&gt;9.5 调度循环&lt;/a&gt; 里遗留了一个问题：如果某个 G 执行时间过长，
其他 G 如何还能被调度？答案绕不开调度理论里的一对老概念，协作式与抢占式。
协作式调度依靠被调度方主动弃权，抢占式调度依靠调度器把被调度方被动中断。&lt;/p&gt;
&lt;p&gt;Go 运行时没有操作系统内核那样的硬件中断能力，基于工作窃取的调度器（&lt;a href=".././steal"&gt;9.2&lt;/a&gt;）
本质上是先来先服务的协作式调度。它如何在不牺牲这一前提的情况下，仍能强行打断一个不肯让权的
G，是本节要讲清的设计。这条线索从一个理论问题出发：运行时凭什么不能在任意一条指令处停下一个
Goroutine。&lt;/p&gt;
&lt;h2 id="971-安全点为何不能想停就停"&gt;9.7.1 安全点：为何不能想停就停&lt;/h2&gt;
&lt;p&gt;抢占的难处不在「中断」，而在「中断之后还要能正确地恢复，并让垃圾回收看懂这个 G 的栈」。
Go 是精确（precise）垃圾回收的语言，GC 必须知道一个被停住的 G，它的每一个栈槽与寄存器里
装的究竟是指针还是普通整数（&lt;a href="../../../part4memory/ch13gc/mark"&gt;13.4&lt;/a&gt;）。这份「此刻哪些位置是
指针」的信息，称为&lt;strong&gt;栈映射&lt;/strong&gt;（stack map）与寄存器映射，由编译器在确定的位置才生成。换言之，
程序并非在每一条机器指令处都备有完整的指针信息，能让 GC 安全扫描的位置是离散的。&lt;/p&gt;
&lt;p&gt;这样的位置就是&lt;strong&gt;安全点&lt;/strong&gt;（safe-point）：线程执行到此，运行时能够完整地辨认其全部对象引用。
不在安全点处贸然停下一个 G，比如恰好停在写屏障序列的中段，或停在一条把指针临时拆成整数运算的
指令之间，扫描就会漏掉或误判指针，破坏 GC 的正确性。安全点把「可以安全停车的地方」限定为一组
离散点，这是抢占一切机制的地基。&lt;/p&gt;
&lt;p&gt;实现安全点有两条路。一条是&lt;strong&gt;轮询式&lt;/strong&gt;（polling），编译器在安全点插入一小段检查代码，G 自己
反复询问「有没有人要我停」，看到请求便配合停下。它简单、可移植，代价是检查指令的常驻开销，
以及一个根本盲区：若一段代码长时间不经过任何安全点（比如一个不含函数调用的紧凑循环），轮询
永远不会被执行到，请求就石沉大海。另一条是&lt;strong&gt;抢占式&lt;/strong&gt;（preemptive），由外部线程强行中断目标，
再设法把它「挪」到一个安全点上，代价是中断时机不可控，正卡在不安全的指令上时必须有办法识别
并放弃。&lt;/p&gt;
&lt;p&gt;为何一个迟迟不到安全点的线程会拖累全局？因为许多运行时操作要求&lt;strong&gt;全局停顿&lt;/strong&gt;（stop-the-world，
STW），典型如 GC 的某些阶段。STW 要等到&lt;strong&gt;所有&lt;/strong&gt; G 都停在安全点才能开始，于是总耗时由最慢的那个
线程决定。这个量有个名字，&lt;strong&gt;到达安全点的时间&lt;/strong&gt;（time-to-safepoint，TTSP）。一个 TTSP 失控的
线程，比如陷在死循环里，会把整次 STW 的延迟拖到不可接受。降低 TTSP 的尾部，正是抢占机制要解决的
工程目标。&lt;/p&gt;
&lt;h2 id="972-协作式抢占搭栈增长检查的便车"&gt;9.7.2 协作式抢占：搭栈增长检查的便车&lt;/h2&gt;
&lt;p&gt;Go 最早的抢占是纯协作式的，且复用了一处现成的安全点：函数序言里的栈增长检查
（&lt;a href="../../../part4memory/ch14stack/readme"&gt;14 执行栈管理&lt;/a&gt;）。每个非 &lt;code&gt;nosplit&lt;/code&gt; 函数在入口都会比较栈指针 SP 与
&lt;code&gt;g.stackguard0&lt;/code&gt;，SP 越界就触发 &lt;code&gt;morestack&lt;/code&gt;，转入 &lt;code&gt;newstack&lt;/code&gt; 去扩张栈。这处检查恰好是一个同步
安全点：此刻 G 的栈是完整可扫描的。&lt;/p&gt;</description></item><item><title>9.8 系统监控</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/sysmon/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/sysmon/</guid><description>&lt;h1 id="98-系统监控"&gt;9.8 系统监控&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;调度器的常规路径已在 &lt;a href=".././schedule"&gt;9.4 调度循环&lt;/a&gt; 讲清：一个 M 绑定一个 P，
从队列里取 Goroutine，运行，再取下一个。这条路径有一个前提，它本身得有机会跑起来。
可一旦所有 P 都陷进长时间的系统调用，或某个 Goroutine 死循环占着 P 不放，常规调度便
卡住了，没有谁来抢回 P，也没有谁来 poll 网络。换句话说，协作式的、跑在 P 上的逻辑，
管不了「P 本身都动不了」这种局面。&lt;/p&gt;
&lt;p&gt;Go 的答案是在调度循环之外另设一条命脉。运行时启动时，&lt;code&gt;main&lt;/code&gt; 用 &lt;code&gt;newm&lt;/code&gt; 拉起一个特殊的 M，
专门运行 &lt;code&gt;sysmon&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#008000"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#00f"&gt;if&lt;/span&gt; haveSysmon {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		systemstack(&lt;span style="color:#00f"&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			newm(sysmon, &lt;span style="color:#00f"&gt;nil&lt;/span&gt;, -1) &lt;span style="color:#008000"&gt;// 一个不绑定 P 的特殊 M&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#008000"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&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;这个 M 的特殊之处在于它&lt;strong&gt;不持有 P，也永不进入常规调度&lt;/strong&gt;。它靠运行时自带的通知机制
（Linux 上是 futex）睡眠与唤醒，不依赖调度器，因此即便所有 P 都被卡住，它照样醒来巡视一圈。
这正是 watchdog（看门狗）的设计：把监督者放在被监督系统之外，它才看得见系统的停摆。&lt;/p&gt;</description></item><item><title>9.9 网络轮询器</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/poller/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/poller/</guid><description>&lt;h1 id="99-网络轮询器"&gt;9.9 网络轮询器&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26，源码事实核对自 &lt;code&gt;src/runtime/netpoll.go&lt;/code&gt; 及各平台实现
（&lt;code&gt;netpoll_epoll.go&lt;/code&gt;、&lt;code&gt;netpoll_kqueue.go&lt;/code&gt; 等）与 &lt;code&gt;src/internal/poll/fd_unix.go&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go 的网络代码看起来是阻塞式的：&lt;code&gt;conn.Read&lt;/code&gt; 会「卡」在那里等数据。可如果它真卡住了所在的
操作系统线程，那么一万个等待网络的 goroutine 就要占用一万个线程，&lt;a href=".././model"&gt;9.1&lt;/a&gt; 苦心经营的
M:N 模型瞬间崩塌。让阻塞式写法仍能规模化的，是网络轮询器（netpoller）。它背后是一段关于
「如何用少数线程照看海量连接」的漫长历史，本节先把这段历史与它背后的设计轴讲清楚，
再看 Go 如何把成熟的事件机制藏进运行时，让你写同步代码、跑事件驱动的 I/O。&lt;/p&gt;
&lt;h2 id="991-c10k-与就绪通知的演化"&gt;9.9.1 C10k 与就绪通知的演化&lt;/h2&gt;
&lt;p&gt;2000 年前后，Dan Kegel 提出了著名的 &lt;strong&gt;C10k 问题&lt;/strong&gt;：一台服务器能否同时处理一万个并发连接？
他的论点是当时的硬件其实够用，瓶颈在软件的 I/O 策略。最朴素的「一连接一线程」模型撑不住：
每个线程预留以兆字节计的栈，一万个线程就是数 GB；加上内核在成千上万个线程间切换的开销，
调度器很快不堪重负。出路是&lt;strong&gt;基于就绪的多路复用&lt;/strong&gt;：让少数线程照看大量 fd，只在某个 fd 就绪时
才去动它。&lt;/p&gt;
&lt;p&gt;就绪通知机制本身也经历了演化。把代价写成大 O 最能看清这条主线。设并发连接数为 $n$，
某一刻就绪的连接数为 $k$（通常 $k \ll n$）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt; / &lt;code&gt;poll&lt;/code&gt;：&lt;strong&gt;每次调用 $O(n)$&lt;/strong&gt;。调用方每次都要把整个 fd 集合交给内核，内核线性扫
一遍标出就绪者，返回后调用方再扫一遍找出它们。集合本身在用户态与内核态间反复拷贝，
连接一多，这种全量扫描与拷贝就成了瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll&lt;/code&gt;（Linux，Libenzi，开发版内核 2.5.44 / 2002，稳定版 2.6.0 / 2003）：兴趣集合通过
&lt;code&gt;epoll_ctl&lt;/code&gt; 在内核里&lt;strong&gt;登记一次&lt;/strong&gt;并常驻，&lt;code&gt;epoll_wait&lt;/code&gt; 只返回&lt;strong&gt;就绪的&lt;/strong&gt;那些 fd，单次调用的
代价是 $O(k)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个常见的不精确说法是「epoll 是 $O(1)$」。更准确的表述是：登记的成本被 &lt;code&gt;epoll_ctl&lt;/code&gt;
摊销，&lt;code&gt;epoll_wait&lt;/code&gt; 的代价正比于&lt;strong&gt;就绪事件数 $k$&lt;/strong&gt;，而非 fd 总数 $n$。正是这一点把
&lt;code&gt;select&lt;/code&gt;/&lt;code&gt;poll&lt;/code&gt; 的「每调用 $O(n)$」改进成了「每调用 $O(k)$」，在 $k \ll n$ 的长连接场景里
是数量级的差距。&lt;/p&gt;</description></item><item><title>9.10 计时器</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/timer/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/timer/</guid><description>&lt;h1 id="910-计时器"&gt;9.10 计时器&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;time.Sleep&lt;/code&gt;、&lt;code&gt;time.After&lt;/code&gt;、&lt;code&gt;time.Timer&lt;/code&gt;、&lt;code&gt;time.Ticker&lt;/code&gt;，乃至网络读写的 &lt;code&gt;SetDeadline&lt;/code&gt;，背后都是
同一套计时器机制。它要回答一个看似简单、实则微妙的数据结构问题：成千上万个计时器同时存在时，
如何高效地知道「下一个该在什么时候叫醒谁」，又不为此空耗一个线程。这一节从这个抽象问题讲起，
看清各种解法的取舍，再落到 Go 的选择与它的演进。&lt;/p&gt;
&lt;h2 id="9101-计时器的数据结构问题"&gt;9.10.1 计时器的数据结构问题&lt;/h2&gt;
&lt;p&gt;一套计时器要支持三种操作：START（登记一个超时）、STOP（到期前取消）、到期检查 / EXPIRY
（时钟前进，触发所有到点的）。难点在于时钟可能每秒被检查成千上万次，无论是否真有计时器到期，
所以「每次 tick」的代价必须低，同时 START / STOP 也要快。几种朴素方案的复杂度对比，正是
Varghese 与 Lauck 1987 年那篇经典论文的出发点：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;方案&lt;/th&gt;
 &lt;th&gt;START&lt;/th&gt;
 &lt;th&gt;STOP&lt;/th&gt;
 &lt;th&gt;每 tick 检查&lt;/th&gt;
 &lt;th&gt;取出到期&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;无序链表&lt;/td&gt;
 &lt;td&gt;$O(1)$&lt;/td&gt;
 &lt;td&gt;$O(1)$&lt;/td&gt;
 &lt;td&gt;$O(n)$ 扫描&lt;/td&gt;
 &lt;td&gt;$O(n)$&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;有序链表&lt;/td&gt;
 &lt;td&gt;$O(n)$&lt;/td&gt;
 &lt;td&gt;$O(1)$&lt;/td&gt;
 &lt;td&gt;$O(1)$ 看表头&lt;/td&gt;
 &lt;td&gt;$O(1)$&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;最小堆&lt;/td&gt;
 &lt;td&gt;$O(\log n)$&lt;/td&gt;
 &lt;td&gt;$O(\log n)$&lt;/td&gt;
 &lt;td&gt;$O(1)$ 看堆顶&lt;/td&gt;
 &lt;td&gt;每个 $O(\log n)$&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里有一个常被混淆的精确之处：堆的「取最小」是 $O(1)$，但每触发一个到期计时器要付 $O(\log n)$
的删除与下沉。所以「每 tick 便宜」只是说检查便宜，真正排空 $k$ 个到期者是 $O(k \log n)$。三种
朴素方案各有所长，却没有一种在 START、STOP 与到期检查上同时取胜，时间轮正是为打破这个僵局
而生。&lt;/p&gt;</description></item><item><title>9.11 NUMA 感知与调度器的未来</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/numa/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/numa/</guid><description>&lt;h1 id="911-numa-感知与调度器的未来"&gt;9.11 NUMA 感知与调度器的未来&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前面各节描述的调度器有一个从未明说的假设：任何 M 访问内存的速度都一样快，任何两个 P 之间
搬动一个 G 的代价都相同。在笔记本和单路服务器上，这个假设近乎成立。可一旦把程序放到大型
多路服务器上，它就开始失真，而且核数越多、失真越大。本节谈这道裂缝：它从何而来，Go 调度器
为何至今对它视而不见，一份曾被认真设计却没有落地的 NUMA 感知方案，以及今天的用户该如何
绕过去。&lt;/p&gt;
&lt;h2 id="9111-numa内存不再是一块平地"&gt;9.11.1 NUMA：内存不再是一块平地&lt;/h2&gt;
&lt;p&gt;早年的对称多处理（SMP）机器里，所有 CPU 经一条共享总线访问同一块内存，访存延迟与是哪个核
发起的无关。这种「一块平地」的内存模型简单，却随核数增长而失效：共享总线成了瓶颈，每多一个
核就多一分争用。&lt;strong&gt;NUMA（non-uniform memory access，非均匀访存）&lt;/strong&gt; 是工业界给出的答案。
把机器拆成若干 &lt;strong&gt;节点&lt;/strong&gt;（node），每个节点是一个 socket 加上直连在它身上的一片本地内存，
节点之间用片间互连（Intel 的 UPI、AMD 的 Infinity Fabric）连成一张网。CPU 访问本节点的
内存走最短路径，访问别节点的内存则要跨越互连，多一跳甚至多几跳。&lt;/p&gt;

&lt;script src="http://golang.design/under-the-hood/mermaid.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
 window.addEventListener("DOMContentLoaded", function () {
 mermaid.initialize({
 startOnLoad: false,
 theme: "neutral",
 securityLevel: "strict",
 fontFamily: "inherit",
 });
 mermaid.run({ querySelector: ".mermaid" });
 });
&lt;/script&gt;


&lt;pre class="mermaid"&gt;flowchart LR
 subgraph N0[&amp;#34;NUMA 节点 0&amp;#34;]
 direction TB
 C0[&amp;#34;CPU 0（多核 &amp;#43; LLC）&amp;#34;]
 M0[(&amp;#34;本地内存 0&amp;#34;)]
 C0 ---|&amp;#34;本地：快&amp;#34;| M0
 end
 subgraph N1[&amp;#34;NUMA 节点 1&amp;#34;]
 direction TB
 C1[&amp;#34;CPU 1（多核 &amp;#43; LLC）&amp;#34;]
 M1[(&amp;#34;本地内存 1&amp;#34;)]
 C1 ---|&amp;#34;本地：快&amp;#34;| M1
 end
 C0 -. &amp;#34;远程：跨互连，更慢&amp;#34; .-&amp;gt; M1
 C1 -. &amp;#34;远程：跨互连，更慢&amp;#34; .-&amp;gt; M0&lt;/pre&gt;
&lt;p&gt;代价是实打实的。本地与远程的访存延迟通常相差一到两倍，跨节点带宽也低于本地带宽，具体数字
随平台而变，可在 Linux 上用 &lt;code&gt;numactl --hardware&lt;/code&gt; 读出节点拓扑与节点间的相对距离矩阵。
比延迟更隐蔽的是 &lt;strong&gt;缓存一致性的开销&lt;/strong&gt;：当一个核要写一行被别的节点缓存着的数据时，一致性协议
（MESI 一族）要先让远端那份副本失效，这条「失效消息」也得跨互连往返。于是 NUMA 上的伪共享
（false sharing，&lt;a href="../../../part4memory/ch12alloc/component"&gt;12.2&lt;/a&gt;）比 SMP 上更疼：同一缓存行
被两个节点的核反复争夺，每次写都要付一次跨节点的一致性往返。&lt;/p&gt;</description></item><item><title>9.12 进一步阅读的参考文献</title><link>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/ref/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part3concurrency/ch09sched/ref/</guid><description>&lt;h1 id="912-进一步阅读的参考文献"&gt;9.12 进一步阅读的参考文献&lt;/h1&gt;
&lt;table class="bib"&gt;
&lt;tr&gt;
&lt;td&gt;[Robert et al., 1999]&lt;/td&gt;&lt;td&gt;Robert D. Blumofe and Charles E. Leiserson. 1999. "Scheduling multithreaded computations by work stealing." J. ACM 46, 5 (September 1999), 720-748. &lt;a href="https://dl.acm.org/citation.cfm?id=324234"&gt;https://dl.acm.org/citation.cfm?id=324234&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Mullender and Cox, 2008]&lt;/td&gt;&lt;td&gt;Mullender, Sape, and Russ Cox. "Semaphores in plan 9." 3rd International Workshop on Plan. Vol. 9. 2008. &lt;a href="https://swtch.com/semaphore.pdf"&gt;https://swtch.com/semaphore.pdf&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Stevens et al., 2008]&lt;/td&gt;&lt;td&gt;Stevens, W. Richard, and Stephen A. Rago. "Advanced programming in the UNIX environment." Addison-Wesley, 2008.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Cox, 2008]&lt;/td&gt;&lt;td&gt;Russ Cox, "Clean up scheduler." Aug 5, 2008. &lt;a href="https://github.com/golang/go/commit/96824000ed89d13665f6f24ddc10b3bf812e7f47#diff-1fe527a413d9f1c2e5e22e08e605a192"&gt;https://github.com/golang/go/commit/96824000ed89d13665f6f24ddc10b3bf812e7f47#diff-1fe527a413d9f1c2e5e22e08e605a192&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Cox, 2009]&lt;/td&gt;&lt;td&gt;Russ Cox, things are much better now, Nov 11, 2009. &lt;a href="https://github.com/golang/go/commit/fe1e49241c04c748d0e3f4762925241adcb8d7da"&gt;https://github.com/golang/go/commit/fe1e49241c04c748d0e3f4762925241adcb8d7da&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Drepper, 2011]&lt;/td&gt;&lt;td&gt;Ulrich Drepper. "Futexes are tricky." Red Hat Inc, Nov 5, 2011. &lt;a href="http://people.redhat.com/drepper/futex.pdf"&gt;http://people.redhat.com/drepper/futex.pdf&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Vyukov, 2012]&lt;/td&gt;&lt;td&gt;Dmitry Vyukov. "Scalable Go Scheduler Design Doc." May 2, 2012. &lt;a href="https://golang.org/s/go11sched"&gt;https://golang.org/s/go11sched&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Vyukov, 2013a]&lt;/td&gt;&lt;td&gt;Dmitry Vyukov, "runtime: improved scheduler." Mar 1, 2013. &lt;a href="https://github.com/golang/go/commit/779c45a50700bda0f6ec98429720802e6c1624e8"&gt;https://github.com/golang/go/commit/779c45a50700bda0f6ec98429720802e6c1624e8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Vyukov, 2013b]&lt;/td&gt;&lt;td&gt;Dmitry Vyukov. "Go Preemptive Scheduler Design Doc." May 15, 2013. &lt;a href="https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#heading=h.3pilqarbrc9h"&gt;https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#heading=h.3pilqarbrc9h&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Vyukov, 2013c]&lt;/td&gt;&lt;td&gt;Dmitry Vyukov. "runtime: make timers faster." Aug 24, 2013. &lt;a href="https://golang.org/issue/6239"&gt;https://golang.org/issue/6239&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Vyukov, 2014]&lt;/td&gt;&lt;td&gt;Dmitry Vyukov, "NUMA-aware scheduler for Go." Sep 2014. &lt;a href="https://docs.google.com/document/u/0/d/1d3iI2QWURgDIsSR6G2275vMeQ_X7w-qxM2Vp7iGwwuM/pub"&gt;https://docs.google.com/document/u/0/d/1d3iI2QWURgDIsSR6G2275vMeQ_X7w-qxM2Vp7iGwwuM/pub&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015]&lt;/td&gt;&lt;td&gt;Austin Clements. "runtime: tight loops should be preemptible." May 26, 2015. &lt;a href="https://golang.org/issue/10958"&gt;https://golang.org/issue/10958&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Lopez et al., 2016]&lt;/td&gt;&lt;td&gt;Brian Lopez et al. "runtime: let idle OS threads exit." Mar 2, 2016. &lt;a href="https://golang.org/issue/14592"&gt;https://golang.org/issue/14592&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Valialkin, 2016]&lt;/td&gt;&lt;td&gt;Aliaksandr Valialkin et al. "runtime: timer doesn't scale on multi-CPU systems with a lot of timers." Apr 5, 2016. &lt;a href="https://golang.org/issue/15133"&gt;https://golang.org/issue/15133&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Taylor et al., 2016]&lt;/td&gt;&lt;td&gt;Ian Lance Taylor et al. "runtime: unexpectedly large slowdown with runtime.LockOSThread." Nov 22, 2016. &lt;a href="https://golang.org/issue/18023"&gt;https://golang.org/issue/18023&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Hofer et al., 2016]&lt;/td&gt;&lt;td&gt;Phil Hofer et al. "runtime: scheduler is slow when goroutines are frequently woken." Dec 7, 2016. &lt;a href="https://golang.org/issue/18237"&gt;https://golang.org/issue/18237&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Chase, 2017]&lt;/td&gt;&lt;td&gt;David Chase. "cmd/compile: loop preemption with fault branch on amd64." May 09, 2019. &lt;a href="https://golang.org/cl/43050"&gt;https://golang.org/cl/43050&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Mills, 2017]&lt;/td&gt;&lt;td&gt;Bryan C. Mills. "proposal: runtime: pair LockOSThread, UnlockOSThread calls." May 22, 2017. &lt;a href="https://golang.org/issue/20458"&gt;https://golang.org/issue/20458&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Rgooch et al., 2017]&lt;/td&gt;&lt;td&gt;rgooch et al. "runtime: terminate locked OS thread if its goroutine exits." May 17, 2017. &lt;a href="https://golang.org/issue/20395"&gt;https://golang.org/issue/20395&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Smelkov et al., 2017]&lt;/td&gt;&lt;td&gt;Kirill Smelkov. "runtime: big performance penalty with runtime.LockOSThread." Sep 10, 2017. &lt;a href="https://golang.org/issue/21827"&gt;https://golang.org/issue/21827&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[lni et al, 2018]&lt;/td&gt;&lt;td&gt;lni et al. "time: excessive CPU usage when using Ticker and Sleep." Sep 17, 2018. &lt;a href="https://golang.org/issue/27707"&gt;https://golang.org/issue/27707&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Hines et al., 2018]&lt;/td&gt;&lt;td&gt;Chris Hines et al. "runtime: scheduler work stealing slow for high GOMAXPROCS." Nov 15, 2018. &lt;a href="https://golang.org/issue/28808"&gt;https://golang.org/issue/28808&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2019]&lt;/td&gt;&lt;td&gt;Austin Clements. "Proposal: Non-cooperative goroutine preemption." January 18, 2019. &lt;a href="https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption"&gt;https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;!-- Brad Fitzpatrick. May, 2016. “net: add mechanism to wait for readability on a TCPConn” &lt;a href="https://github.com/golang/go/issues/15735"&gt;https://github.com/golang/go/issues/15735&lt;/a&gt;
Ian Lance Taylor. Feb 11, 2017. “os: use poller for file I/O” &lt;a href="https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac"&gt;https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac&lt;/a&gt;
Ian Lance Taylor. Apr 3, 2019. “runtime: change netpoll to take an amount of time to block” &lt;a href="https://github.com/golang/go/commit/831e3cfaa594ceb70c3cbeff2d31fddcd9a25a5e"&gt;https://github.com/golang/go/commit/831e3cfaa594ceb70c3cbeff2d31fddcd9a25a5e&lt;/a&gt;
“The Go netpoller” &lt;a href="https://morsmachine.dk/netpoller"&gt;https://morsmachine.dk/netpoller&lt;/a&gt;
Wikipedia: File descriptor &lt;a href="https://en.wikipedia.org/wiki/File_descriptor"&gt;https://en.wikipedia.org/wiki/File_descriptor&lt;/a&gt;

SELECT(2) · Linux Programmer's Manual &lt;a href="http://man7.org/linux/man-pages/man2/select.2.html"&gt;http://man7.org/linux/man-pages/man2/select.2.html&lt;/a&gt;

Ian Lance Taylor. Apr 3, 2019. “runtime: change netpoll to take an amount of time to block” &lt;a href="https://github.com/golang/go/commit/831e3cfaa594ceb70c3cbeff2d31fddcd9a25a5e"&gt;https://github.com/golang/go/commit/831e3cfaa594ceb70c3cbeff2d31fddcd9a25a5e&lt;/a&gt;

Ian Lance Taylor. Apr 6, 2019. “runtime: add netpollBreak” &lt;a href="https://github.com/golang/go/commit/50f4896b72d16b6538178c8ca851b20655075b7f"&gt;https://github.com/golang/go/commit/50f4896b72d16b6538178c8ca851b20655075b7f&lt;/a&gt;

Dmitry Vyukov. Oct 31, 2018. “runtime: don't recreate netpoll timers if they don't change” &lt;a href="https://github.com/golang/go/commit/86d375498fa377c7d81c5b93750e8dce2389500e"&gt;https://github.com/golang/go/commit/86d375498fa377c7d81c5b93750e8dce2389500e&lt;/a&gt; --&gt;</description></item></channel></rss>