3. M(机器线程):M的结构体字段解析、M的创建与复用、M与系统线程的绑定关系
3.1 M 结构体核心字段解析
在 Go 运行时中,m 结构体代表一个操作系统线程(Machine)。它是调度器三大核心组件之一,负责承载 P(处理器)和 G(协程)的执行。M 本身并不执行代码,而是通过绑定系统线程来提供执行能力。
以下为 m 结构体在 src/runtime/runtime2.go 中的核心字段及其作用:
| 字段名 | 类型 | 作用描述 |
|---|---|---|
g0 |
*g | M 持有的特殊 G,用于执行调度、垃圾回收等系统任务,拥有独立的栈空间。 |
curg |
*g | 当前正在 M 上执行的用户 G 的指针。 |
p |
puintptr | 当前 M 绑定的 P 的指针。M 必须绑定一个 P 才能执行用户 G。 |
nextp |
puintptr | 预绑定的 P,用于在 M 被唤醒后快速绑定,减少查找开销。 |
spinning |
bool | 标记 M 是否处于自旋状态(正在寻找可运行的 G 但尚未找到)。 |
blocked |
bool | 标记 M 是否因系统调用或 IO 操作而阻塞。 |
parking |
bool | 标记 M 是否正在进入休眠状态。 |
lockedg |
guintptr | 当 G 调用 runtime.LockOSThread() 后,M 与 G 锁定,此字段指向该 G。 |
createstack |
stack | M 创建时的栈信息,用于调试和崩溃回溯。 |
freelink |
*m | 空闲 M 链表指针,用于 M 的复用池。 |
mOS |
mOS | 操作系统相关的私有字段,如线程句柄、信号掩码等。 |
关键理解:
g0 和 curg 是 M 上两个最重要的 G 指针。M 通过 g0 执行调度逻辑,通过 curg 执行用户代码。两者栈空间不同,切换时需进行栈切换。
3.2 M 的创建与复用机制
Go 运行时不会为每个 G 都创建新的 M,而是采用“按需创建 + 空闲复用”的策略,以降低系统线程创建和销毁的开销。
3.2.1 M 的创建流程
M 的创建入口是 newm(fn, pp) 函数,调用链如下:
- 触发条件:当调度器发现没有空闲 M 可用,且需要新的执行线程时(如启动新 P、处理系统调用阻塞等)。
- 分配 m 结构体:调用
allocm()从堆上分配m结构体,并初始化g0栈。 - 创建系统线程:调用
newosproc(mp),通过操作系统 API(如 Linux 的clone)创建真正的内核线程。 - 线程入口:新线程启动后执行
mstart(),进入调度循环。
创建代价: 每次
newm 都会产生一次系统调用,涉及内核栈分配、线程描述符创建等操作,开销约为微秒级。因此复用 M 对性能至关重要。
3.2.2 M 的复用池
当 M 完成任务且没有 G 可执行时,它不会立即销毁,而是进入空闲链表等待复用。复用机制通过以下字段和函数实现:
| 组件 | 说明 |
|---|---|
sched.midle |
全局空闲 M 链表头,所有空闲 M 通过 freelink 字段串联。 |
mput(mp) |
将 M 放入空闲链表,同时减少全局自旋 M 计数。 |
mget() |
从空闲链表头部取出一个 M,若链表为空则返回 nil。 |
needm |
当 M 被唤醒时,从空闲链表获取或创建新 M。 |
复用流程如下:
- M 执行完所有 G 且无 P 可绑定 → 调用
mput进入空闲链表。 - 调度器需要新 M → 先调用
mget尝试获取空闲 M。 - 若获取成功 → 直接唤醒该 M 的系统线程,避免创建开销。
- 若获取失败 → 调用
newm创建新 M。
注意: 空闲 M 仍然持有系统线程资源(内核栈、线程描述符),因此大量空闲 M 会占用系统资源。Go 通过
maxmcount 限制最大 M 数量(默认 10000),防止资源耗尽。
3.3 M 与系统线程的绑定关系
M 与操作系统线程之间是 1:1 绑定 关系。每个 M 结构体在创建时都会对应一个真实的 OS 线程,直到 M 被销毁或线程退出。
3.3.1 绑定实现
在 newosproc 函数中,Go 运行时通过平台特定的系统调用创建线程,并将线程的入口函数设置为 mstart。线程启动后,其线程局部存储(TLS)中会保存 M 的指针,使得运行时可以随时通过 getg() 获取当前线程绑定的 M 和 G。
// 简化伪代码:Linux 下 newosproc 的实现
func newosproc(mp *m) {
// 创建线程,栈大小为 2MB
ret := clone(cloneFlags, stackTop, mp, &mstart)
// 线程启动后,mp 被保存在 TLS 中
}
3.3.2 绑定生命周期
M 与系统线程的绑定从创建开始,到 M 被回收结束。期间 M 可以处于以下状态:
| M 状态 | 系统线程状态 | 说明 |
|---|---|---|
| 运行中 | 运行中 | M 正在执行 G 或调度代码。 |
| 自旋 | 运行中 | M 在寻找 G,但尚未找到,线程仍在 CPU 上轮询。 |
| 阻塞 | 阻塞 | M 因系统调用或锁而阻塞,线程进入内核等待状态。 |
| 空闲 | 休眠 | M 在空闲链表中,线程通过 futex 或 pthread_cond_wait 休眠。 |
| 销毁中 | 退出中 | M 被标记为销毁,线程执行退出系统调用。 |
3.3.3 锁线程(LockOSThread)
当 G 调用 runtime.LockOSThread() 时,M 与 G 绑定,且该 M 不再被调度器复用。这种机制用于需要线程局部存储或特定线程亲和性的场景(如 CGO 回调、图形库绑定)。
// G 锁定当前 M
runtime.LockOSThread()
// 此时 lockedg 指向当前 G,该 M 不会进入空闲链表
// 直到 G 调用 UnlockOSThread() 或退出
性能影响: 锁线程会导致该 M 无法被其他 G 使用,可能造成线程资源浪费。建议仅在必要时使用,并尽快解锁。
3.4 M 与系统线程绑定关系图
下图展示了 M 结构体与系统线程之间的绑定关系,以及 M 在不同状态下的线程行为:
3.5 M 复用与创建流程图
下图展示了调度器在需要 M 时的决策流程,体现“优先复用、按需创建”的核心策略:
3.6 关键设计原则
- M 不执行代码,只提供执行上下文:M 本身只是一个结构体,真正的执行能力来自绑定的系统线程。
- M 与 P 解耦:M 可以绑定不同的 P,P 也可以绑定不同的 M,这种解耦使得调度器可以灵活地迁移执行资源。
- M 的复用减少系统调用:通过空闲链表复用 M,避免了频繁的线程创建和销毁,降低了操作系统负载。
- M 数量受控:通过
maxmcount和自旋 M 计数,防止 M 数量过多导致系统资源耗尽。
总结: M 是 Go 调度器中连接用户态调度与内核线程的桥梁。理解 M 的结构、创建复用机制以及与系统线程的绑定关系,是掌握 Go 调度器工作原理的关键一步。M 的复用策略直接影响了 Go 程序在高并发场景下的线程开销和调度效率。