【计算机技术】线程/协程/纤程/虚拟线程

1 缘起

在软件世界里,性能与并发始终是绕不开的主题。从最早的操作系统线程,到语言层的协程,再到更轻量的纤程,以及近年来备受关注的虚拟线程,每一次抽象层级的提升,都是为了让程序能以更低的成本、更高的效率处理更多任务。随着硬件发展放缓、应用规模不断扩大,我们不再单纯依赖"更快的 CPU",而是必须学会更聪明地使用计算资源。理解线程、协程、纤程与虚拟线程的演进,不只是掌握几个名词,而是看清现代并发模型背后的设计哲学:如何在复杂性、性能与开发体验之间找到平衡。也正因此,重新梳理这些概念,成为每一个开发者迈向更高层次并发编程的必经之路。

2 内容角度

  • 概念
  • 调度方式
  • 上下文切换
  • 栈模型
  • 存在问题及解决方案

3 概念

3.1 一句话概念

  • 线程:OS 调度的最小执行单元

  • 协程:用户态调度的轻量执行单元

  • 纤程:协程的一种底层实现方式(用户态栈切换)

  • 虚拟线程:JVM 提供的轻量线程(语义是线程,性能像协程)

3.2 概念详解

3.2.1 线程

  • 线程运行在内核态,由操作系统负责调度(抢占式)。
  • 多个线程共享同一进程的内存空间,但每个线程有独立的栈。
  • 创建成本高、切换成本高(需要内核态切换)。
  • 适合 CPU 密集型任务。

3.2.2 协程

  • 不由操作系统调度,而是由语言运行时或框架调度(协作式)。
  • 切换不需要进入内核态 → 成本极低(纳秒级)。
  • 栈空间小(KB 级),可支持百万级并发。
  • 适合 I/O 密集型任务(如网络请求、爬虫、服务端高并发)。

3.2.3 纤程

  • 纤程是协程的一种更底层的形式,强调"用户态栈切换"。
  • 切换时保存/恢复寄存器、栈指针等,不进入内核。
  • 需要开发者或运行时显式让出执行权(协作式)。
  • 常见于 C++、游戏引擎、Boost Fiber 等。

3.2.4 虚拟线程

  • 虚拟线程不是 OS 线程,而是 JVM 内部结构。
  • JVM 会把虚拟线程挂在少量 OS 线程上执行(类似 M:N 模型)。
  • 阻塞不会占用 OS 线程(JVM 会自动挂起虚拟线程)。
  • 创建成本极低,可支持百万级并发。
  • 适合 Java 服务端高并发场景(Web 服务、数据库访问)。

3.2.5 对比

特性 线程 协程 纤程 虚拟线程
调度者 OS 用户态 用户态 JVM
调度方式 抢占式 协作式 协作式 混合式
切换成本 高 极低 极低 极低
栈大小 MB KB KB KB(分段)
并发数量 万级 百万级 百万级 百万级
阻塞行为 占用 OS 线程 需非阻塞 I/O 需非阻塞 I/O 不占用 OS 线程
典型语言 C++/Java Go/Python/Kotlin C++/系统库 Java Loom

4 调度方式

4.1 线程

  • 调度者:操作系统内核(Kernel Scheduler)
  • 调度方式:抢占式调度(Preemptive Scheduling)
    (1)OS 会根据时间片、优先级等策略强制切换线程
    (2)线程不需要主动让出 CPU
    (3)切换时必须进入内核态,保存/恢复寄存器、栈指针等
  • 特点
    (1)公平性强:不会因为某个线程不让出 CPU 而卡死
    (2)成本高:内核态切换开销大(微秒级)
    (3)适合 CPU 密集型任务

4.2 协程

  • 调度者:用户态运行时(Runtime / Event Loop)
  • 调度方式:协作式调度(Cooperative Scheduling)
    (1)协程必须 主动让出执行权(如 await、yield)
    (2)调度器在用户态切换协程,不进入内核态
    (3)切换只需保存少量寄存器和栈指针
  • 特点
    (1)切换成本极低(纳秒级)
    (2)不公平:如果协程不让出,会阻塞整个调度器
    (3)适合 I/O 密集型任务

4.3 纤程

  • 调度者:用户态(通常由库或框架)
  • 调度方式:协作式调度(Cooperative)
    (1)纤程本质上是协程的一种底层实现,因此调度方式类似:
    (2)完全用户态
    (3)手动切换栈(保存/恢复寄存器、指令指针、栈指针)
    (4)必须主动让出执行权
  • 特点
    (1)比协程更底层
    (2)切换成本极低
    (3)常用于 C++、游戏引擎、Boost Fiber 等

4.4 虚拟线程

  • 调度者:JVM(而不是 OS)
  • 调度方式:混合式调度(Cooperative + OS Scheduling)
    虚拟线程的调度方式非常独特:
    (1) 运行时由 JVM 调度(类似协程)虚拟线程不是 OS 线程,JVM 会把大量虚拟线程挂在少量 OS 线程上执行(M:N 模型)。
    (2)阻塞时自动挂起(协作式)
    当虚拟线程执行阻塞 I/O,JVM 会自动把它挂起,不占用 OS 线程,I/O 完成后再恢复。
    (3)最终仍由 OS 调度底层载体线程(抢占式)虚拟线程运行在 OS 线程上,OS 线程仍然是抢占式调度。
  • 特点
    (1)语义是线程,性能像协程
    (2)阻塞不会占用 OS 线程
    (3)适合 Java 高并发服务

4.5 对比

模型 调度者 调度方式 是否抢占 是否进入内核态 切换成本
线程 OS 抢占式 是 是 高(微秒)
协程 用户态 runtime 协作式 否 否
纤程 用户态 协作式 否 否 极低(纳秒)
虚拟线程 JVM 混合式(协作 + 抢占) 部分 阻塞时否 极低

5 上下文切换

5.1 线程

  • 切换发生位置:内核态(Kernel Mode)

  • 切换内容(非常多)

    线程切换时,操作系统需要保存和恢复:

    • 寄存器(通用寄存器、程序计数器 PC)
    • 栈指针(SP)
    • 内核栈
    • CPU 状态字
    • 内存管理信息(如页表)
    • 调度器元数据(优先级、时间片等)
  • 切换过程

    (1)OS 抢占当前线程

    (2)保存当前线程上下文到内核

    (3)加载另一个线程的上下文

    (4)切回用户态继续执行

  • 特点

    (1)必须进入内核态 → 成本高(微秒级)

    (2)切换内容多

    (3)线程栈大(MB),切换时需要更多维护

5.2 协程

  • 切换发生位置:用户态(User Mode)

  • 切换内容(非常少)

    协程切换只需保存:

    • 少量寄存器(如 PC、SP)
    • 协程栈指针
    • 协程局部状态(由 runtime 管理)
  • 切换过程

    (1)协程主动让出(await / yield)

    (2)runtime 保存当前协程的寄存器和栈指针

    (3)runtime 恢复另一个协程的寄存器和栈指针

    (4)继续执行

  • 特点

    (1)不进入内核态 → 成本极低(纳秒级)

    (2)切换内容少

    (3)栈小(KB),切换轻量

5.3 纤程

  • 切换发生位置:用户态

  • 纤程是协程的底层实现,因此切换方式类似,但更底层。

    切换内容

    • 保存/恢复寄存器
    • 保存/恢复指令指针(IP)
    • 保存/恢复栈指针(SP)
    • 切换用户态栈(纤程有独立栈)
  • 切换过程

    (1)纤程切换通常通过 手动切换栈 完成,例如:

    (2)保存当前栈指针

    (3)切换到另一个纤程的栈

    (4)恢复寄存器

    (5)跳转到新纤程的执行点

  • 特点

    (1)完全用户态 → 不进入内核

    (2)切换成本与协程相当(纳秒级)

    (3)更接近底层,常用于 C++、游戏引擎

5.4 虚拟线程

  • 切换发生位置:JVM 内部(用户态 + 少量内核态)

  • 虚拟线程的切换方式最特别,因为它结合了线程语义和协程性能。

  • 两种切换场景

    (1) 虚拟线程之间的切换(用户态)

    当虚拟线程主动挂起(如 I/O 阻塞)时:

    • JVM 保存虚拟线程的栈帧(分段栈)
    • JVM 切换到另一个虚拟线程
    • 类似协程切换 → 不进入内核态

    (2)虚拟线程绑定的 OS 线程切换(内核态)

    虚拟线程最终运行在 OS 线程上,因此:

    • OS 线程之间的切换仍然是内核态
    • 但虚拟线程数量远大于 OS 线程数量
    • 大部分切换发生在 JVM 内部,而不是 OS
  • 特点

    (1)阻塞不会占用 OS 线程(JVM 会自动挂起虚拟线程)

    (2)大部分切换在 JVM 内部 → 成本接近协程

    (3)保留线程语义(同步代码也能高并发)

5.5 对比

模型 切换位置 是否进入内核态 切换成本 切换内容 本质
线程 内核态 是 高(微秒) 寄存器 + 栈 + 内核状态 OS 调度
协程 用户态 否 极低(纳秒) 寄存器 + 栈指针 用户态调度
纤程 用户态 否 极低(纳秒) 寄存器 + 栈切换 协程底层实现
虚拟线程 JVM 内部 + 少量内核 部分 低 分段栈 + JVM 状态 JVM 版协程

6 栈模型

6.1 线程

  • 栈大小:固定且大(通常 1MB 左右)

    每个线程创建时,OS 会为它分配一个 固定大小的大栈(如 1MB、2MB)。

    栈空间一旦分配,不能动态缩小。

  • 栈结构

    (1)调用栈(函数调用帧)

    (2)局部变量

    (3)返回地址

    (4)异常处理信息

  • 特点

    (1)占用内存大 → 限制线程数量(一般几千到几万)

    (2)切换成本高 → 栈大,切换时需要更多状态维护

    (3)适合 CPU 密集型任务

6.2 协程

  • 栈大小:小且可动态扩容(几 KB 起)

    协程的栈通常只有 几 KB,并且可以按需增长。

    例如:

    Go goroutine:初始栈 2KB,可动态扩容

    Kotlin 协程:栈分段 + continuation 保存状态

    Python asyncio:状态保存在对象中,不依赖传统栈

  • 栈结构

    协程的栈通常是:

    (1)小栈(stacklet)

    (2)分段栈(segmented stack)

    (3)或状态机(async/await 转换)

  • 特点

    (1)占用内存极小 → 可创建百万协程

    (2)切换成本低 → 栈小,切换轻量

    (3)适合 I/O 密集型任务

6.3 纤程

  • 栈大小:小栈(KB 级),独立栈,手动切换

    纤程是协程的底层实现,因此栈模型类似协程,但更底层。

  • 栈结构

    (1)每个纤程有独立的用户态栈

    (2)栈大小通常在 KB 级

    (3)切换时需要手动保存/恢复栈指针(SP)

  • 特点

    (1)完全用户态栈

    (2)切换时直接切换栈指针

    (3)比协程更底层、更灵活

  • 常见于:

    (1)C++ Boost Fiber

    (2)Windows Fiber API

    (3)游戏引擎任务系统

6.4 虚拟线程

  • 栈大小:小栈 + 分段栈(Stack Chunking)

    虚拟线程的栈不是一次性分配,而是 按需分段分配。

    类似协程的分段栈模型。

  • 栈结构

    (1)栈被拆成多个小块(chunk)

    (2)JVM 会在挂起虚拟线程时把栈帧"拆开"并存储到堆中

    (3)恢复时再重新组装

  • 特点

    (1)初始栈很小 → 可创建百万虚拟线程

    (2)阻塞时栈会被挂起到堆中 → 不占用 OS 线程

    (3)保留线程语义 → 同步代码也能高并发

6.5 对比

模型 栈大小 是否可扩容 栈位置 切换成本 并发能力
线程 MB 级 否 OS 分配 高 万级
协程 KB 级 是 用户态 极低 百万级
纤程 KB 级 是 用户态 极低 百万级
虚拟线程 KB 级(分段) 是 JVM 管理 低 百万级

7 应用场景

7.1 实际场景

  • CPU 密集型

    线程(或线程池)

  • I/O 密集型

    → 协程 / 纤程 / 虚拟线程

  • Java 高并发服务

    虚拟线程(Loom)

7.2 编程语言实现

Go:goroutine(协程) + M:N 调度

Python:asyncio 协程

Java:传统线程池 → 虚拟线程(Loom)

C++:Boost Fiber / C++20 coroutine

Rust:async/await + executor

8 常见问题

8.1 线程

8.1.1 问题1:线程数量受限(无法支持高并发)

  • 原因:
    每个线程占用 1MB 左右栈空间
    OS 调度成本高
    创建/销毁成本大
  • 解决:
    使用线程池(Thread Pool)
    控制最大线程数
    使用异步 I/O 或协程替代

8.1.2 问题 2:线程上下文切换开销大

  • 原因:

    切换需要进入内核态

    保存/恢复大量寄存器、栈、调度信息

  • 解决:

    减少线程数量

    使用无锁结构、减少竞争

    使用协程/虚拟线程替代

8.1.3 问题 3:线程泄漏(Thread Leak)

  • 表现:
    线程不断创建但不退出,导致系统资源耗尽。
  • 原因:
    线程未正确结束
    死循环
    阻塞 I/O
    未关闭线程池
  • 解决:
    使用线程池统一管理
    设置超时机制
    避免无限阻塞
    监控线程数量

8.2 协程

8.2.1 问题 1:协程泄漏(Coroutine Leak)

  • 表现:

    协程数量不断增加,最终 OOM。

  • 原因:

    协程未正确结束

    异步任务未 await

    无限挂起(suspend forever)

    忘记取消协程(如 Kotlin 的 Job)

  • 解决:

    使用结构化并发(structured concurrency)

    设置超时(timeout)

    必须 await 或 cancel

    使用协程作用域(scope)管理生命周期

8.2.2 问题 2:协程阻塞导致整个调度器卡死

  • 原因:
    协程是协作式调度,如果一个协程执行阻塞操作(如 sleep、IO),会阻塞整个事件循环。
  • 解决:
    使用非阻塞 I/O
    在专用线程池执行阻塞任务(如 Kotlin 的 Dispatchers.IO)
    使用 async/await 正确让出执行权

8.2.3 问题 3:协程调度不公平

  • 原因:
    协作式调度依赖协程主动让出,如果某个协程不让出,会饿死其他协程。
  • 解决:
    避免长时间 CPU 计算
    使用 yield
    将 CPU 密集型任务放入线程池

8.3 纤程

8.3.1 问题 1:纤程需要手动管理栈,容易出错

  • 原因:
    纤程切换需要手动保存/恢复栈指针、寄存器,容易出现:
    栈溢出
    栈损坏
    未对齐导致崩溃
  • 解决:
    使用成熟的纤程库(Boost Fiber、Windows Fiber)
    避免手写纤程切换逻辑
    使用更高级的协程抽象

8.3.2 问题 2:纤程无法利用多核(默认单线程)

  • 原因:
    纤程调度通常在单线程执行,除非手动绑定多个 OS 线程。
  • 解决:
    使用 M:N 调度模型(如 Go)
    或使用协程框架而不是裸纤程

8.3.3 问题 3:纤程阻塞会阻塞整个线程

  • 原因:
    纤程是用户态调度,如果执行阻塞 I/O,会阻塞整个 OS 线程。
  • 解决:
    使用非阻塞 I/O
    使用 I/O 线程池
    使用更高级的协程框架(如 Go runtime)

8.4 虚拟线程

8.4.1 问题 1:虚拟线程数量过多导致调度压力

  • 原因:
    虽然虚拟线程轻量,但创建百万级虚拟线程仍会给 JVM 带来调度压力。
  • 解决:
    控制虚拟线程数量
    使用虚拟线程池(Executor.newVirtualThreadPerTaskExecutor)
    避免无限创建

8.4.2 问题 2:阻塞操作仍然可能导致性能下降

  • 原因:
    虚拟线程可以挂起,但底层 OS 线程仍可能被某些 native 调用阻塞。
  • 解决:
    避免使用阻塞的 JNI 调用
    使用 Loom 兼容的非阻塞 I/O
    避免 synchronized 长时间锁持有

8.4.3 问题 3:虚拟线程泄漏

  • 原因:

    虚拟线程创建后未结束

    无限等待

    锁竞争导致线程挂起但不退出

  • 解决:

    使用 try-with-resources 管理虚拟线程

    使用结构化并发(StructuredTaskScope)

    监控虚拟线程数量

8.4.4 问题 4:虚拟线程与传统线程池混用导致问题

  • 原因:

    虚拟线程不需要池,但开发者习惯性使用线程池,导致:

    虚拟线程被限制

    失去高并发优势

  • 解决:

    不要为虚拟线程使用固定大小线程池

    使用 per-task executor

8.5 对比

模型 常见问题 根本原因 解决方案
线程 数量受限、泄漏、切换慢 栈大、内核态调度 线程池、减少阻塞
协程 泄漏、阻塞调度器、不公平 协作式调度、用户态 非阻塞 I/O、结构化并发
纤程 栈管理复杂、阻塞问题 手动栈切换、用户态 使用库、避免阻塞
虚拟线程 调度压力、阻塞 JNI、泄漏 JVM 调度、分段栈 结构化并发、避免阻塞调用
相关推荐
ZealSinger13 小时前
Boot4挂起函数丢traceId怎么修
spring boot·kotlin·协程·可观测性
EatFan2 天前
2026 后端架构进入 AI 原生阶段:事件驱动 + 虚拟线程 + Agent 内嵌三驾马车怎么落地
人工智能·架构·agent·虚拟线程·事件驱动·ai原生·后端架构
ZealSinger3 天前
Kotlin后端别再用GlobalScope
kotlin·协程·后端开发·结构化并发
流浪0013 天前
Linux系统篇39——线程(四) pthread库的由来和线程的创建与等待
linux·操作系统·线程·pthread库
流浪0014 天前
Linux系统篇42——线程(七) pthread库管理线程的工作流,从创建到回收
linux·操作系统·线程·线程创建到销毁
沫璃染墨5 天前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
EatFan6 天前
Spring Boot 4 升级避坑指南:依赖变化、Undertow 弃用、4.0.5 补丁与 Java 25 虚拟线程落地
java·spring boot·微服务·虚拟线程·spring boot 4
en.en..6 天前
条件变量(pthread_cond_t):为什么必须用 while,不能用 if
线程
xufengzhu13 天前
Java 虚拟线程实战指南(JDK 21+)
java·虚拟线程·jdk21+
AIgorithmGEEK13 天前
[Linux]线程三部曲(上):一个执行流的诞生——从操作系统一路拆到 pthread_create
linux·线程·pid