
我们常常听说纤程(fibers)是轻量级的线程,但是这一说法还是非常抽象,究竟是什么含义呢?
要理解纤程,可以先了解一下"执行状态保存"、"调度器(scheduler)"这两个核心概念。
核心概念
1. 执行状态保存
程序执行到某一节点时,需要保存当前的"状态快照",包含当前代码执行位置、栈信息、变量值等关键数据,支持程序后续从该节点精准恢复执行。
类比:你正在阅读一本书(程序执行),看到第50页第3段时需要暂停处理其他事,便用书签标记当前位置(状态快照),这个"带书签的当前阅读状态"就是保存的执行状态;后续你可以凭借书签直接回到第50页第3段继续阅读,无需重新从开头翻起。
2. 调度器(scheduler)
负责管理多个并发单元的生命周期,包括分配执行资源、决定执行顺序与时机、处理暂停/恢复逻辑等,核心作用是协调多个任务高效推进。
类比:调度器就像公司的项目协调员,多个员工(并发单元)需要完成不同项目任务(程序任务),协调员会根据任务优先级、员工工作量分配工作时间(执行时机),当员工遇到问题暂停工作时,协调员会安排其他员工接手或待问题解决后提醒其恢复工作(暂停/恢复管理),确保所有任务整体高效推进,避免资源浪费。
纤程的本质
纤程可以理解为:
纤程=执行状态保存机制+调度器\text{纤程} = \text{执行状态保存机制} + \text{调度器}纤程=执行状态保存机制+调度器
为了更好地理解这一概念,我们通过如下两组对比来介绍:
- 纤程 vs 线程
- 纤程 vs 其他并发机制(不同语言中提供的轻量级并发机制)
1. 纤程 vs 线程
坦白地说,在使用层面:纤程 ≈ 线程。
二者的区别是:
- 纤程 :可以理解为用户态线程,调度器运行在用户态,上下文切换(状态的保存/恢复)仅操作用户态资源,无需内核参与,切换成本极低;
- 内核态线程:调度器运行在内核态,上下文切换需陷入内核态(用户态→内核态切换),操作内核栈、页表等内核资源,切换成本高。
这也就是为何我们常常听到:纤程是轻量级的线程。
2. 纤程 vs 其他并发机制(不同语言中提供的轻量级并发机制)
基于上面介绍的执行状态保存和调度器的概念,我们比较 Go (goroutine)、Java Fibers (Project Loom)、Python asyncio、Node.js (JS 异步) 这四种语言中轻量级并发机制:
| 技术栈 | 核心并发单元 | 状态保存方式 | 调度器特征 | 纤程类型 | 与其他的核心差异 |
|---|---|---|---|---|---|
| Go (goroutine) | goroutine | 有栈(stackful) | 运行时(runtime)内置调度器 | 标准纤程 | 调度器是语言运行时级,抢占式调度 |
| Java Fibers (Project Loom) | Fiber | 有栈(stackful) | JVM 内置调度器 | 标准纤程 | 调度器是 JVM 级,可与线程池协同 |
| Python asyncio | 异步函数 | 无栈(stackless) | 事件循环(用户态调度器) | 轻量级纤程 | 调度器是事件循环,协作式调度 |
| Node.js (JS 异步) | 异步函数 | 无栈(stackless) | 事件循环(V8+libuv,用户态调度器) | 轻量级纤程 | 单线程事件循环,协作式调度 |
说明:
- 标准纤程:拥有独立栈空间,可以在任意位置暂停和恢复,调度器完全在用户态运行
- 轻量级纤程:没有独立栈空间,只能在特定标记点(await/yield)暂停,调度器同样在用户态运行
所有这些机制本质上都是用户态的轻量级并发单元,只是实现方式和适用场景不同。
3. 关键差异解析:状态保存方式与调度方式如何影响执行特性?
3.1 状态保存方式:有栈 vs 无栈
状态保存方式直接决定了任务执行的灵活性:
有栈(stackful)
- 每个并发单元拥有独立的栈空间
- 能够捕获完整的调用栈信息,任务可以在任意函数调用层级暂停
- 恢复时无需依赖外部状态,独立性强
- 典型代表:Go 的 goroutine、Java 的 Fiber
- 优势:即使在深度嵌套的函数调用中暂停,也能准确恢复执行
无栈(stackless)
- 不拥有独立栈空间,仅保存必要的局部状态
- 只能在特定的标记点(如 async/await)暂停和恢复
- 内存开销更小,但灵活性受限
- 典型代表:Python asyncio、JavaScript async/await
- 限制:若在未标记的普通函数中执行耗时操作,无法自动暂停,可能阻塞整个事件循环
3.2 调度方式:抢占式 vs 协作式
调度方式影响并发执行的稳定性和效率:
抢占式调度
- 调度器可以主动中断长时间运行的任务
- 无需开发者手动管理调度逻辑,降低开发成本
- 能够防止单个任务独占资源,确保多个任务公平执行
- 典型代表:Go goroutine(Go 1.14+ 支持异步抢占)
- 适用场景:高并发服务端应用,需要高稳定性
协作式调度
- 任务必须主动释放控制权(通过 await/yield)
- 调度开销更小,但需要开发者严格遵守编程规范
- 若某个任务未及时释放控制权,会导致整个调度器阻塞
- 典型代表:Python asyncio、Node.js
- 适用场景:I/O 密集型应用,且团队熟悉异步编程模型
- 注意事项:必须避免在异步函数中执行 CPU 密集型同步操作
总结:如何选择适合的并发机制?
1. 高并发、高稳定性场景(如微服务、实时系统)
推荐:Go goroutine、Java Fiber
- 理由 :
- 有栈 + 抢占式调度,无需担心单个任务阻塞
- 内置调度器降低开发成本
- 可轻松创建数十万并发单元
2. I/O 密集型场景(如 Web API、网络爬虫)
推荐:Python asyncio、Node.js
- 理由 :
- 内存开销极小,适合大量 I/O 等待场景
- 生态成熟,异步库丰富
- 注意事项 :
- 严格避免在异步代码中执行 CPU 密集型同步操作
- 确保所有 I/O 操作都使用异步版本
- 团队需熟悉协作式调度的编程模式
3. 选型建议
- 无绝对优劣:所有这些机制都是用户态的轻量级并发实现,只是权衡了不同的设计目标
- 根据场景选择 :
- 需要强稳定性 → 选择抢占式调度(Go/Java)
- 极致轻量 + I/O 密集 → 选择协作式调度(Python/Node.js)
- 团队能力:选择团队熟悉且生态完善的技术栈