引擎盖下的引擎:Go 的 GMP、Java 的锁和 Erlang 的堆
I’m happy to translate the article for you, but I need the actual text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line, formatting, markdown, and any code blocks exactly as they are while translating the rest into Simplified Chinese.
Introduction
作为后端工程师,我们常常把 并发 当作黑盒:我们写 go func(){} 或 spawn() 并期待奇迹。了解运行时如何调度这些任务,使高级工程师与架构师区别开来。
Source: …
GMP 调度器
Go 的调度器遵循 G‑M‑P 模型:
| 组件 | 描述 |
|---|---|
| G(Goroutine) | 轻量级用户空间线程(起始约 2 KB 栈)。保存指令指针和栈。 |
| M(Machine) | 由内核管理的 OS 线程。它是真正执行 CPU 指令的工作者。 |
| P(Processor) | 拥有本地运行队列和一部分内存缓存的逻辑标记。执行 G 必须先持有一个 P。 |
- 规则: M 必须拥有 P 才能运行 G。
- P = 逻辑核心数: 默认情况下
GOMAXPROCS等于 CPU 核心数,限制并行度但允许无限并发。
何时创建 G?
每当调用 go func(){} 时就会创建一个 goroutine。它由 Go 运行时在用户空间分配,约占 2 KB,并被放入当前 P 的本地运行队列。
何时创建 M?
运行时会保持 M 的数量较低,仅在以下情况出现时才会生成新的 OS 线程:
- 某个 goroutine 进行阻塞系统调用(例如 CGO、重文件 I/O),且该调用无法异步处理。
- 当前 M 卡在 OS 内核内部。
- 其他 P 正在等待工作但没有可用的 M(创建新 M 成本较高,约 1–2 MB)。
观察者:sysmon 与 SIGURG
sysmon 是什么?
sysmon(system monitor)是一个特殊的运行时线程,不持有 P,在专用的 M 上运行。它会定期(20 µs – 10 ms)唤醒,以强制公平性。
抢占是如何工作的
自 Go 1.14 起,调度器使用信号来强制工作窃取:
sysmon扫描所有 Ps。如果它发现某个 goroutine 在处理器上运行时间超过 10 ms,就向执行该 goroutine 的 M 发送 SIGURG。- 为什么是 SIGURG?
- 带外信号:现代应用很少使用,因此不会与用户信号冲突。
- 非破坏性:不同于
SIGINT,它不会终止进程。 - 与 libc 安全:对使用 CGO 的程序安全。
- 操作系统中断该 M;Go 的信号处理器在 goroutine 的栈上注入对
asyncPreempt的调用。 - goroutine 让出 CPU,被移动到全局运行队列,P 选择新的 G 来运行。
模型比较:“通过共享内存进行通信” vs. “通过通信共享内存”
Go / Java:共享堆
所有线程共享同一块堆。数据通过修改共享对象来传递。
失效模式(Java 示例)
// Java: 显式锁定(瓶颈)
class Counter {
private int count = 0;
// synchronized 强制操作系统暂停其他线程(上下文切换)
public synchronized void increment() {
count++;
}
}
- 竞争条件: 忘记使用
synchronized会导致数据损坏。 - 性能: 锁需要操作系统介入,耗费数千个周期。
- 死锁: 循环等待会使应用冻结。
Erlang:私有堆
每个进程拥有自己的堆,消除了“嘈杂邻居”效应。
为什么 Erlang 更“好”(银行示例)
-module(bank_server).
-behaviour(gen_server).
%% 1. 安全的银行进程
init([]) -> {ok, 100}. %% 余额为 100 美元
%% 2. 危险的崩溃进程
trigger_crash() ->
spawn(fun() ->
%% A. 这将在私有堆上分配 1 GB
CrashList = lists:seq(1, 100000000),
%% B. 立即崩溃
1 / 0
end).
- 分配: 被生成的进程在其私有堆上分配 1 GB。若在 Java/Go 中,这会填满全局堆并触发一次 stop‑the‑world GC。
- 崩溃: 进程因除零而死亡。
- 清理: Erlang VM 只需丢弃该私有堆。
- 零 GC 成本: 不需要扫描其他进程的内存。
- 零影响:
bank_server仍然以微秒级延迟继续处理 100 美元的余额,完全不受崩溃影响。
关键要点
- Java 的共享内存模型 给工程师带来了沉重的正确性负担,使得大规模并发更难以推理。
- Erlang 在可靠性方面表现出色,因为私有堆防止了“嘈杂的邻居”影响整个系统。
- Go 提供了务实的折中方案:它使用共享堆以获得原始速度(无需数据复制),同时鼓励 CSP 风格的通信(通道),以避免显式锁的复杂性。