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

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 调度、分段栈 结构化并发、避免阻塞调用
相关推荐
小毅&Nora1 天前
【后端】【C++】协程深度解析:从内部机制到实用场景
c++·协程
武藤一雄2 天前
C# 中线程安全都有哪些
后端·安全·微软·c#·.net·.netcore·线程
程序员龙一3 天前
进程、线程、协程通俗讲解与对比
c++·线程·进程·协程
enjoy编程4 天前
Spring AI 深度重构 renren-security,基于 Java 21 虚拟线程打造极致高并发脚手架
java·spring boot·spring·重构·虚拟线程·spring boot 4·virtual thread
ComputerInBook4 天前
C++ 标准提供的 thread (线程)之 join() 函数示例(windows平台)
c++·线程·join函数
再睡一夏就好8 天前
深入Linux线程:从轻量级进程到双TCB架构
linux·运维·服务器·c++·学习·架构·线程
添砖java‘’9 天前
Linux线程控制全解析
linux·c++·线程
ベadvance courageouslyミ10 天前
线程控制(同步相关)
线程·同步
charlee4410 天前
为什么协程能让程序不再卡顿?——从同步、异步到 C++ 实战
qt·协程·异步编程·gui卡顿·boost.coroutine2