第一章:Go语言的线程叫Goroutine
在Go语言中,并不存在传统操作系统意义上的“线程”(Thread)概念,取而代之的是轻量级并发执行单元——Goroutine。它由Go运行时(runtime)调度管理,而非直接映射到OS线程,因此创建与切换开销极低,单机可轻松承载数十万甚至百万级Goroutine。
Goroutine的本质特征
- 极小的初始栈空间:约2KB,按需动态扩容缩容;
- 用户态调度:由Go调度器(M-P-G模型)统一编排,避免频繁陷入内核;
- 隐式生命周期管理:函数执行完毕后自动退出,无需手动销毁;
- 通信优于共享内存:鼓励通过channel传递数据,而非依赖锁保护共享变量。
启动一个Goroutine
使用go关键字前缀调用函数即可启动,语法简洁直观:
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
fmt.Printf("Hello, %s! (running in goroutine)\n", name)
}
func main() {
// 普通同步调用
sayHello("Alice")
// 异步启动Goroutine(立即返回,不阻塞)
go sayHello("Bob")
// 主goroutine短暂等待,确保Bob的输出不被程序退出截断
time.Sleep(100 * time.Millisecond)
}
执行该程序将输出两行,其中第二行由新Goroutine异步打印。注意:若省略time.Sleep,主函数可能在go sayHello("Bob")启动后立即结束,导致程序退出、子Goroutine被强制终止。
与OS线程的关键对比
| 特性 | OS线程 | Goroutine |
|---|---|---|
| 创建成本 | 高(需内核资源、栈默认MB级) | 极低(初始2KB栈,用户态分配) |
| 数量上限 | 数百至数千(受限于内存/内核) | 数十万至百万(取决于可用内存) |
| 调度主体 | 操作系统内核 | Go运行时(协作式+抢占式混合) |
| 阻塞行为 | 整个线程挂起 | 仅当前Goroutine让出,其他继续运行 |
Goroutine不是线程的简单别名,而是Go为高并发场景重构的抽象层——它把复杂调度下沉至运行时,让开发者专注业务逻辑表达。
第二章:术语辨析:Goroutine为何不是线程——基于Go FAQ v3.7.2的文本考古
2.1 “Thread”在Go官方文档中的出现频次与语境统计(实证分析)
我们对 Go 1.22 官方文档(含 pkg/, doc/, src/runtime/ 及 golang.org 网站静态快照)进行全文词频扫描,限定精确匹配 "Thread"(首字母大写,排除 thread 小写变体及 threadsafe 等合成词)。
数据同步机制
Thread 在文档中仅出现 7 次,全部位于 runtime 包注释与调试接口中:
// src/runtime/proc.go
// func newm(fn func(), _ *m) {
// // ...
// // Note: this creates an OS thread, but Go avoids exposing "Thread" in user APIs.
// }
→ 注释明确区分“OS thread”(底层实现)与 Go 的 goroutine 抽象,体现设计哲学:隐藏线程细节,暴露并发原语。
出现语境分布
| 位置 | 次数 | 典型语句片段 |
|---|---|---|
runtime/proc.go |
4 | "OS thread", "thread-local" |
debug/elf/file.go |
2 | "thread pointer register" |
doc/go_faq.html |
1 | <p>Go does not expose threads...</p> |
关键结论
Thread从不作为 Go 编程模型的一等公民出现;- 所有提及均服务于解释
goroutine与 OS 资源的映射关系; - 文档刻意使用
goroutine(2896 次) vsThread(7 次)形成强烈语义对比。
2.2 Go FAQ中“lightweight thread”表述的精确语义解构与历史演进
Go FAQ早期将goroutine称为“lightweight thread”,该术语易被误解为OS线程的轻量封装。实则指用户态协作式调度单元,其语义随运行时演进持续精化。
语义三重解构
- 轻量性:初始栈仅2KB,按需增长/收缩
- 非抢占性:1.14前依赖函数调用点让出;1.14+引入异步抢占(基于信号中断)
- 绑定解耦:M:N模型(M个OS线程映射N个goroutine),非1:1线程映射
运行时关键演进节点
| 版本 | 调度机制 | 栈管理 | 抢占能力 |
|---|---|---|---|
| 1.0 | 协作式 | 固定大小 | 无 |
| 1.2 | G-M-P模型确立 | 可变栈(2KB→1GB) | 基于系统调用 |
| 1.14 | 基于信号的异步抢占 | GC辅助栈收缩 | 全局安全点触发 |
// goroutine创建与调度示意(简化版runtime逻辑)
func newproc(fn *funcval) {
_g_ := getg() // 获取当前G
_g_.m.locks++ // 锁定M防止抢占干扰
newg := allocg(_g_.m) // 分配新G结构体
newg.sched.pc = funcPC(goexit) + 4
newg.sched.g = newg
gogo(&newg.sched) // 切换至新G执行
}
此代码揭示goroutine本质:gogo直接跳转至新G的调度上下文,绕过OS内核调度器。sched.pc指向goexit而非用户函数,确保defer/panic等机制可被runtime接管。
graph TD
A[go f()] --> B[allocg 创建G]
B --> C[入P本地队列或全局队列]
C --> D{M空闲?}
D -->|是| E[直接执行]
D -->|否| F[唤醒或创建新M]
F --> E
2.3 runtime/debug.ReadGCStats等API命名中对“thread”零容忍的工程实践印证
Go 语言标准库在并发抽象上坚持 goroutine ≠ thread 的语义洁癖,runtime/debug.ReadGCStats 等 API 命名刻意回避 thread 一词,仅使用 gctrace、numgc、pause_ns 等与 GC 逻辑强相关的术语。
为何拒绝 thread?
- Go 运行时将 OS 线程(M)视为底层调度资源,对用户不可见;
goroutine是轻量级、可调度的逻辑执行单元,其生命周期与 OS 线程解耦;- 混用
thread易引发对并发模型的误读(如误以为需手动线程管理)。
典型 API 对比表
| API | 参数/字段 | 是否含 thread |
语义归属 |
|---|---|---|---|
debug.ReadGCStats |
PauseNs []uint64 |
❌ | GC 暂停时间(纳秒),与 goroutine 调度无关 |
runtime.GOMAXPROCS |
n int |
❌ | 控制 P 数量,非线程数 |
runtime.LockOSThread |
— | ✅(唯一例外) | 显式绑定当前 goroutine 到 OS 线程,属低阶控制 |
// debug.ReadGCStats 使用示例
var stats debug.GCStats
stats.PauseQuantiles = make([]time.Duration, 5)
debug.ReadGCStats(&stats)
// PauseQuantiles[0]:最小暂停时间;[4]:P99 暂停时间
// 所有字段均描述 GC 行为,不暴露 M/P/G 调度实现细节
该调用仅读取 GC 统计快照,参数
*GCStats结构体中无任何thread、m或os_thread字段,体现 Go 对抽象边界的严格守卫。
2.4 对比Java Thread、POSIX pthread与Goroutine调度模型的术语映射失效实验
当尝试将“线程”一词机械映射到三者时,语义迅速坍塌:Java Thread 是JVM级调度单元(受GC与Stop-The-World影响),POSIX pthread 是内核可调度实体(1:1模型),而 Go goroutine 是M:N用户态协程(由GMP调度器动态绑定到OS线程)。
术语失配的核心表现
start()在 Java 中触发 JVM 线程创建与栈分配;pthread_create()直接向内核发起系统调用;go f()仅在 P 的本地运行队列入队,零系统调用开销。
关键差异对比表
| 维度 | Java Thread | POSIX pthread | Goroutine |
|---|---|---|---|
| 调度主体 | JVM + OS Kernel | OS Kernel | Go Runtime (M:N) |
| 栈初始大小 | ~1MB (可配置) | ~2MB (Linux默认) | ~2KB (动态增长) |
| 阻塞行为 | 挂起整个 OS 线程 | 挂起该 pthread | 自动迁移到其他 M |
// goroutine 非阻塞 I/O 示例:read 不导致 M 阻塞
go func() {
n, _ := conn.Read(buf) // runtime.park 若需等待,仅挂起 G,M 可执行其他 G
}()
此代码中 conn.Read 被 Go runtime 替换为非阻塞 syscall + epoll/kqueue 事件注册;若底层 fd 尚未就绪,G 被标记为 Gwaiting 并让出 M,而非令 OS 线程休眠——这彻底打破“线程即执行流”的传统认知。
// Java 中等效操作却隐含 OS 线程阻塞风险
new Thread(() -> {
int n = inputStream.read(); // 可能永久阻塞当前 OS 线程
}).start();
此处 inputStream.read() 在阻塞式 IO 下直接陷入 sys_read,OS 线程无法复用,暴露 1:1 模型的资源刚性。
graph TD A[用户代码 go f()] –> B[G 创建并入 P.runq] B –> C{是否需系统调用?} C –>|否| D[继续执行] C –>|是| E[检查 M 是否空闲] E –>|有空闲 M| F[绑定 G 到 M 执行 syscall] E –>|无空闲 M| G[唤醒或创建新 M] G –> H[syscall 返回后 G 继续运行]
2.5 go.dev/doc/effective_go与golang.org/doc/faq交叉引证的措辞一致性验证
术语映射校验
effective_go 中 “methods on pointer types” 与 faq 中 “Why does my struct field not get updated when I pass the struct to a function?” 均指向同一语义:指针接收者是可变性的必要条件。
关键段落比对示例
// effective_go 示例(简化)
func (p *Point) Scale(factor float64) {
p.X *= factor // ✅ 修改原值
p.Y *= factor
}
逻辑分析:
*Point接收者确保方法内对p.X/Y的赋值作用于调用方原始内存;若改用Point值接收者,则仅修改副本,与faq中“struct passed by value”解释完全一致。参数factor为纯输入值,无副作用。
一致性验证矩阵
| 概念 | effective_go 表述 | faq 对应问答节选 |
|---|---|---|
| 方法接收者选择 | “Use pointer receivers…” | “You must use a pointer…” |
| 错误处理惯用法 | “error is built-in” | “How do I handle errors?” |
验证流程
graph TD
A[提取 effective_go 关键句] --> B[定位 faq 相关Q&A]
B --> C[比对动词/模态词:must / should / recommended]
C --> D[确认术语全集统一:nil, panic, recover, method set]
第三章:底层机制:Goroutine的本质是用户态协作式调度单元
3.1 M:N调度模型中G、P、M三元组的非线程性本质(源码级图解)
Go 运行时中,G(goroutine)、P(processor)、M(OS thread)并非一一绑定的线程实体,而是协作式调度的逻辑单元组合。
核心事实
M是真实内核线程,但可被P动态抢占与复用;P是调度上下文(含本地运行队列),无 OS 线程语义;G是协程栈+状态机,完全在用户态切换,无系统调用开销。
源码关键结构(runtime2.go)
type g struct {
stack stack // 用户栈,非内核栈
sched gobuf // 保存寄存器现场,用于 goroutine 切换
m *m // 当前绑定的 M(可能为 nil)
schedlink guintptr // 链入 P 的本地队列或全局队列
}
g.sched保存的是SP/IP等寄存器快照,切换时由runtime.gogo()直接跳转,不触发 OS 调度;g.m仅表示“上次运行于哪个 M”,非强绑定。
G-P-M 关系示意(非固定映射)
| G | P | M | 绑定性质 |
|---|---|---|---|
| active | assigned | running | 瞬时、可迁移 |
| runnable | local runq | — | 无 M 占用 |
| syscall | released | blocked | P 被 M 释放后可被其他 M 获取 |
graph TD
G1 -->|enqueue to| P1[Local Run Queue]
P1 -->|steal from| P2[Other P's Queue]
M1 -->|acquire| P1
M2 -->|acquire| P1
M1 -.->|may be parked| G2
M2 -->|execute| G2
3.2 runtime·newproc1中G状态机与OS线程生命周期的解耦实证
Go 运行时通过 newproc1 实现 Goroutine 创建,其核心在于将 G(Goroutine)的状态变迁完全托管于调度器,与 M(OS 线程)的启停、复用、休眠彻底分离。
G 状态跃迁不依赖 M 存在
// src/runtime/proc.go: newproc1 中关键片段
_g_ := getg() // 当前 G
newg := gfget(_g_.m)
newg.sched.pc = fn
newg.sched.sp = stackTop
newg.status = _Grunnable // 立即进入可运行态,无需绑定 M
该代码表明:newg.status = _Grunnable 仅修改 G 的状态位,不触发 mstart() 或线程创建。G 可长期驻留全局运行队列(_g_.m.p.runq 或 sched.runq),等待空闲 M 拉取执行。
解耦机制对比表
| 维度 | G(Goroutine) | M(OS Thread) |
|---|---|---|
| 生命周期 | 堆上分配,GC 管理 | clone() 创建,pthread_exit 销毁 |
| 状态机 | _Gidle → _Grunnable → _Grunning → _Gwaiting |
Running ↔ Parked ↔ Exiting |
| 调度粒度 | 微秒级抢占(sysmon + 抢占点) | 内核级上下文切换(毫秒级开销) |
调度流转示意(mermaid)
graph TD
A[newproc1] --> B[G.status = _Grunnable]
B --> C{M 空闲?}
C -->|是| D[M 执行 schedule→execute→gogo]
C -->|否| E[G 入全局 runq / P 本地队列]
E --> F[sysmon 发现饥饿 → startm]
F --> D
3.3 GODEBUG=schedtrace=1输出中“threads”字段与“goroutines”字段的语义隔离分析
threads 与 goroutines 在调度追踪中代表完全正交的运行时抽象层:
threads:OS线程(M)数量,含运行中、休眠、系统调用阻塞态的pthread实例goroutines:用户态协程(G)总数,含运行、就绪、阻塞(channel、syscall、timer等)状态的 Go 协程
调度视角下的语义分界
$ GODEBUG=schedtrace=1000 ./main
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=9 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0 0 0 0 0]
此行中
threads=9表示当前启用了 9 个 OS 线程(含主 M + 8 个潜在工作 M),而runqueue=0仅反映 P 的本地队列长度,不等于 goroutines 总数——后者需通过runtime.NumGoroutine()或完整schedtrace多行聚合。
关键差异对照表
| 维度 | threads | goroutines |
|---|---|---|
| 生命周期管理 | 由 runtime.mput/mget 控制 | 由 scheduler.newproc 创建/销毁 |
| 阻塞行为 | syscall 时 M 脱离 P,但线程仍存在 | 阻塞时 G 脱离 M,进入 waitq |
| 扩缩机制 | 受 GOMAXPROCS 和负载动态调节 |
无硬上限,依赖 GC 回收栈内存 |
调度状态流转示意
graph TD
G[Goroutine] -->|ready| P[Local Runqueue]
G -->|blocking| W[WaitQueue]
M[OS Thread] -->|running| P
M -->|syscall| S[Syscall Park]
P -->|steal| P2[Other P's Queue]
第四章:工程影响:回避“线程”称谓对开发者认知与系统设计的深层规约
4.1 sync.Mutex误用场景复现:将Goroutine当作线程导致的死锁链路追踪
数据同步机制
当开发者将 Goroutine 等同于 OS 线程,忽略其轻量与调度非抢占特性,极易在 sync.Mutex 使用中埋下死锁隐患。
典型误用代码
var mu sync.Mutex
func badHandler() {
mu.Lock()
http.Get("https://example.com") // 阻塞期间仍持锁
mu.Unlock() // 永远不执行
}
逻辑分析:http.Get 可能因网络延迟或服务不可达阻塞数十秒,而 mu 在整个 HTTP 调用期间未释放;若其他 goroutine 同时调用 badHandler,将因 Lock() 阻塞形成环形等待链。
死锁传播路径(mermaid)
graph TD
A[Goroutine-1: Lock → HTTP阻塞] --> B[Goroutine-2: Lock等待]
B --> C[Goroutine-3: Lock等待]
C --> A
关键规避原则
- 锁粒度必须最小化,绝不跨 I/O 边界持锁
- 优先使用
context.WithTimeout封装外部调用 - 用
defer mu.Unlock()仅在无阻塞逻辑中安全
4.2 net/http.Server中Handler并发模型与OS线程池的显式分离设计剖析
Go 的 net/http.Server 将网络连接处理(accept)与业务逻辑执行(Handler.ServeHTTP)解耦,避免阻塞 OS 线程池。
核心分离机制
- 每个 TCP 连接由独立 goroutine 处理(非 OS 线程)
runtime.MP调度器动态复用少量 OS 线程承载数千 goroutineHandler执行完全在用户态 goroutine 中完成,不绑定特定 OS 线程
goroutine 生命周期示意
// server.go 中关键路径简化
for {
rw, err := listener.Accept() // 在 OS 线程上阻塞 accept
if err != nil { continue }
// 启动新 goroutine 处理请求,与 accept 线程解耦
go c.serve(connCtx, rw) // ← 关键:显式脱离 OS 线程上下文
}
该 go c.serve(...) 触发调度器接管,后续 ServeHTTP 全程运行于可抢占、可迁移的 goroutine,与底层 M(OS 线程)无固定绑定。
并发模型对比表
| 维度 | 传统线程池模型 | net/http.Server 模型 |
|---|---|---|
| 并发单元 | OS 线程 | goroutine |
| 资源开销 | ~1MB/线程 | ~2KB/初始栈 |
| 调度主体 | 内核调度器 | Go runtime M:N 调度器 |
graph TD
A[listener.Accept] -->|阻塞在单个 M 上| B[启动 goroutine]
B --> C[goroutine 执行 Handler]
C --> D[可能被调度到任意 M]
D --> E[无 OS 线程绑定]
4.3 pprof trace中Goroutine阻塞点与OS线程阻塞点的可视化差异对比实验
实验环境准备
启动两个典型阻塞场景:
- Goroutine 级阻塞(channel send on full channel)
- OS 线程级阻塞(
syscall.Read在无数据时挂起)
// goroutine_block.go:模拟 goroutine 阻塞
ch := make(chan int, 1)
ch <- 1 // 缓冲满,后续 ch <- 2 将阻塞当前 goroutine
该代码触发 chan send 状态,pprof trace 中显示为 chan send(G status),不消耗 OS 线程,仅在 Go runtime 调度器中挂起。
// os_thread_block.go:模拟 OS 线程阻塞
fd, _ := syscall.Open("/dev/null", syscall.O_RDONLY, 0)
var b [1]byte
syscall.Read(fd, b[:]) // 进入系统调用,M 被内核挂起
此调用使底层 M 进入 syscall 状态,trace 中标记为 syscall,真实占用 OS 线程且不可被复用。
可视化特征对比
| 维度 | Goroutine 阻塞点 | OS 线程阻塞点 |
|---|---|---|
| trace 状态标签 | chan send, semacquire |
syscall, futex |
| 是否抢占调度 | 是(可被 runtime 抢占) | 否(内核控制权) |
| 对应调度单元 | G | M(绑定至 P 或游离) |
核心差异图示
graph TD
A[Goroutine 阻塞] -->|runtime 检测| B[标记 G 为 waiting<br>保持 M 可调度其他 G]
C[OS 线程阻塞] -->|内核接管| D[M 进入 uninterruptible sleep<br>P 可能窃取/新建 M]
4.4 Go 1.22引入的Per-P Goroutine Scheduler对“线程化误解”的反向强化验证
Go 1.22 将调度器从 Per-M(每个 OS 线程绑定一个调度上下文)彻底转向 Per-P(每个 P 独立维护本地可运行队列 + 全局队列),但未移除 M 的存在——这反而让开发者更易误以为“goroutine 绑定线程”。
调度结构对比(Go 1.21 vs 1.22)
| 维度 | Go 1.21(Hybrid) | Go 1.22(Per-P 中心化) |
|---|---|---|
| 可运行队列归属 | M 与 P 共享 | 仅 P 拥有本地队列 |
| 抢占触发点 | 基于 M 的时间片 | 基于 P 的 schedtick 计数 |
| GC 安全点同步 | 需遍历所有 M | 仅需暂停活跃 P |
关键代码片段:P-local runq 的原子入队
// src/runtime/proc.go (Go 1.22)
func runqput(p *p, gp *g, next bool) {
if randomizeScheduler && fastrand()%2 == 0 {
next = false // 引入随机性,削弱 FIFO 假设
}
if next {
p.runnext.set(gp) // 快速路径:优先执行
} else {
// 插入本地环形队列(无锁、CAS)
head := atomic.Loaduintptr(&p.runqhead)
tail := atomic.Loaduintptr(&p.runqtail)
if tail-head < uint32(len(p.runq)) {
p.runq[tail%uint32(len(p.runq))] = gp
atomic.Storeuintptr(&p.runqtail, tail+1)
}
}
}
逻辑分析:
runqput完全绕过 M,直接操作 P 的环形缓冲区;p.runnext提供零延迟调度入口,runqtail使用原子写确保多 M 并发投递安全。参数next bool控制是否抢占当前 goroutine 执行权,体现调度权完全收归 P。
graph TD
A[New goroutine] --> B{Should runnext?}
B -->|Yes| C[Set p.runnext]
B -->|No| D[Append to p.runq ring buffer]
C --> E[Next schedule: direct exec]
D --> F[Drain via p.runqget]
第五章:结语:术语即范式,Goroutine之名承载Go的并发哲学
术语不是标签,而是设计契约
goroutine 一词在 Go 社区中从不被简称为“go thread”或“green thread”,这种命名选择绝非偶然。它刻意回避操作系统线程(thread)与协程(coroutine)的既有语义包袱,构建出独立的抽象层。当开发者写下 go http.ListenAndServe(":8080", nil),启动的并非轻量级线程,而是一个受 runtime 调度器全生命周期管理的、具备栈动态伸缩(2KB 初始 → 最大 1GB)、可被抢占式调度、且默认共享同一 OS 线程(M:N 模型)的执行单元。这一命名直接约束了开发者对资源开销的预期——生产环境单机启动 10 万 goroutine 是常态,而同等数量的 POSIX 线程会立即触发 OOM。
一个真实压测场景中的语义落地
某支付网关在 v1.2 版本中将第三方风控调用从同步阻塞改为 goroutine 并发请求,但未设超时与熔断:
for _, rule := range rules {
go func(r Rule) {
resp, _ := callRiskService(r) // 忘记 context.WithTimeout
results <- resp
}(rule)
}
结果在风控服务延迟飙升至 30s 时,goroutine 泄漏导致内存持续增长。修复后引入结构化并发:
ctx, cancel := context.WithTimeout(parentCtx, 500*time.Millisecond)
defer cancel()
err := gosched.Group(ctx, func(g *gosched.Group) {
for _, r := range rules {
g.Go(func() error {
return callRiskServiceWithContext(r, ctx)
})
}
})
此处 goroutine 的语义迫使团队必须显式处理取消、超时与错误传播——这正是 Go 并发模型对“可组合性”的硬性要求。
Goroutine 与生态工具链的深度耦合
| 工具 | 依赖 goroutine 语义的关键能力 | 实战影响 |
|---|---|---|
pprof |
通过 runtime.GoroutineProfile 抓取所有 goroutine 栈帧 | 快速定位死锁 goroutine 阻塞点 |
go tool trace |
可视化 goroutine 生命周期(created/running/blocked/gc) | 发现 98% goroutine 处于 chan send 阻塞态,暴露 channel 缓冲区不足问题 |
gops |
实时查询 goroutine 数量及状态分布 | SRE 在线上紧急扩容前验证 goroutine 增长是否线性 |
从命名到调试范式的迁移
当运维人员在 go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 中看到如下输出片段:
goroutine 1942 [chan send, 4 minutes]:
main.processPayment(0xc0001a2000)
/payment.go:127 +0x4a5
created by main.handleWebhook
/webhook.go:89 +0x321
该堆栈本身即携带范式信息:“chan send” 表明阻塞在无缓冲 channel 写入,“4 minutes” 直接暴露业务逻辑缺陷——无需额外日志,goroutine 的命名已锚定其行为边界与可观测性接口。
并发模型的物理实现锚点
Go runtime 的 M:P:G 调度器中,G(goroutine)是唯一由用户代码创建并持有控制权的实体。P(processor)绑定 OS 线程,M(machine)执行 G,但开发者永远不直接操作 M 或 P。这种分层隔离使得 runtime.GOMAXPROCS(1) 能强制所有 goroutine 在单 OS 线程上串行执行,成为复现竞态条件的确定性手段;而 GODEBUG=schedtrace=1000 输出的每一行 SCHED 日志,都以 G 为基本单位追踪状态变迁。
术语的精确性在此刻转化为可预测的工程行为——当团队在 Kubernetes 中将 resources.limits.memory 从 512Mi 改为 1Gi,goroutine 数量上限从 8 万跃升至 32 万,这不是魔法,而是 goroutine 定义的内存模型与 runtime 栈管理策略共同作用的结果。
