进程、线程和协程的工作原理

学习进程、线程和协程的异同之处,通过三种爬虫程序的编写、运行与性能对比加深理解


学习资源

进程、线程与协程的异同

进程、线程和协程是三种不同粒度的执行单元,它们在资源隔离、调度方式和并发模型上有本质区别。

维度进程线程协程
调度者操作系统内核操作系统内核用户态程序(运行时/应用)
资源拥有独立地址空间(页表、文件描述符等)共享进程地址空间,拥有独立栈和寄存器共享进程/线程地址空间,仅拥有独立栈
创建/切换开销高(需切换页表、刷新 TLB)中(需内核介入、保存/恢复寄存器)极低(纯用户态,仅保存少量寄存器)
栈大小独立地址空间~2 MB(虚拟,按需分配)~数 KB(初始,可动态增长)
通信方式IPC(管道、共享内存、socket 等)共享内存(需同步原语)共享内存(通常无需锁,协作式)
是否可并行是(多核)是(多核)否(单线程内为协作式并发)
能否利用多核依赖运行时调度到多线程上
隔离性强(独立地址空间,互不影响)中(共享地址空间,一个崩溃会影响整个进程)弱(共享进程/线程状态)

核心区别

  1. 调度层级不同:进程和线程由操作系统内核调度(抢占式),协程由用户态程序调度(协作式)。协程主动 yieldawait 时让出执行权,而非被时钟中断强制切换。

  2. 上下文切换开销:进程切换需要切换页表、刷新 TLB,开销最大。线程切换需要内核态/用户态切换,保存/恢复寄存器,开销中等。协程切换在用户态完成,仅需保存少量寄存器和栈指针,开销可低至数十纳秒。

  3. 并发模型:进程是操作系统级别的资源隔离单位,适合强隔离场景。线程是 CPU 调度的基本单位,适合中等并发。协程是编程语言层面的控制流抽象,适合 I/O 密集型高并发场景。

  4. 栈管理:进程和线程的栈由内核管理,虚拟地址空间预留较大(线程默认 2 MB)。协程的栈由运行时管理,初始分配极小(如 Rust 的 async block 栈仅数 KB),可在堆上动态增长,极适合创建大量并发任务。

如何选择

  • 进程:需要强隔离(如不同用户的程序、浏览器标签页)、独立的安全边界
  • 线程:需要利用多核并行计算、CPU 密集型任务、中等等并发量
  • 协程:I/O 密集型高并发(Web 服务器、爬虫、消息队列)、需要大量轻量级任务

Rust 中三种模型均可直接使用:std::process::Command(进程)、std::thread(线程)、async/await + Tokio(协程)。本任务中的三个爬虫程序正是这三种模型的具体实现。

实践任务

编写爬虫程序,依据高校名称和官方网站列表中的网站链接,从对应网站上下载首页的纯文本内容,保存在本地当前目录下,文件命名为学校的中文名称

分别使用三种并发模型实现:

  1. 基于进程的爬虫程序(串行)
  2. 基于线程的爬虫程序(每请求一线程)
  3. 基于协程的爬虫程序(async/await)

对比三种模型的性能特征:延时分布吞吐率内存开销

代码实现位于 labs/crawler/,包含三种实现的完整 Rust 源码。


三种爬虫模型的性能对比分析

测试环境

  • 项目web crawler — Rust 爬虫,分别基于进程、线程、协程实现
  • 数据集:34 所中国高校官网首页
  • HTTP 客户端reqwest,超时 2 秒,重定向限制 5 次
  • 运行时:线程模型使用 std::thread::scope,协程模型使用 Tokio + futures::join_all
  • 编译cargo build --release(Release 模式)
  • 操作系统:Linux x86_64

1. 总耗时(吞吐率)

模型总耗时相对比例
进程(串行)20~23 s≈ 1×(基准)
线程(并行)~2.2 s10×
协程(异步)~2.0 s11×

结论:进程模型逐请求串行阻塞等待,总耗时 ≈ 所有请求延迟之和。线程和协程模型并发执行所有请求,总耗时 ≈ 最慢单个请求的延迟,吞吐率提升约一个数量级。

2. 延时分布

由于线程和协程模型并发执行、不串行等待每个请求完成,当前版本的代码无法像进程模型那样逐个测量延迟。

进程模型的延时分布(两次运行):

指标第一次第二次说明
P500.59 s0.65 s半数请求 0.6 s 左右完成
P900.95 s1.44 s90% 请求在 1.4 s 内
P991.32 s3.32 s尾部延迟波动较大,受网络状况影响
总耗时20.23 s27.30 s累加所有延迟的结果

总耗时(2027 s)远大于 P99(1.33.3 s),直观地展示了串行模型的瓶颈——即使大部分请求很快,总时间也是所有延迟之和。

线程和协程模型也可增加逐个记录的延迟统计,当前版本做了简化。

3. 内存开销

模型最大 RSS(常驻内存)相对比例
进程(串行)~7.8 MB≈ 1×(基准)
线程(并行)~13.4 MB≈ 1.7×
协程(异步)~13.7 MB≈ 1.8×
  • 进程模型内存最小:串行执行,任何时候最多只有一个 HTTP 响应驻留,无需并发调度结构。
  • 线程模型内存稍大:每线程拥有独立栈空间(默认 2 MB 虚拟,常驻按需分配)。
  • 协程模型与线程模型在此规模下接近:Tokio 运行时自身包含线程池和 I/O 驱动,但在本测试的 34 并发量下差异不明显。协程的栈极轻量(初始仅数 KB),在更大并发规模下优势会显著体现。

4. CPU 与上下文切换

模型User CPUSystem CPUCPU 利用率自愿上下文切换
进程0.13 s0.13 s1%3831
线程0.07 s0.12 s9%2582
协程0.05 s0.10 s8%1982
  • CPU 利用率普遍很低:三个模型均为 I/O 密集型,CPU 大部分时间等待网络响应。
  • 上下文切换:进程模型循环读写阻塞,每次 I/O 等待都触发调度,自愿上下文切换最多。线程和协程模型总耗时短,上下文切换反而更少。
  • 协程 User CPU 最低(0.05 s),体现了异步 I/O 的最少调度开销。

5. 综合对比

维度进程(串行)线程(并行)协程(异步)
总耗时(34 个请求)~23 s~2.2 s~2.0 s
吞吐率≈ 1.5 req/s≈ 15 req/s≈ 17 req/s
内存开销7.8 MB13.4 MB13.7 MB
代码复杂度低(直观易懂)中(Arc + 闭包)中(async/await
实现机制串行阻塞 I/Oblocking 线程池非阻塞 epoll + 协程
适用场景请求量小、调试中等并发、通用场景高并发、I/O 密集型

6. 关键发现

  1. 并发 vs 串行是最大差异:进程模型串行执行,总耗时 = 所有请求延迟之和。线程和协程并发执行,总耗时 ≈ 最慢请求的延迟。这是性能差异的根本来源。

  2. 线程 vs 协程在此规模下性能几乎相同:对于 34 个并发请求,操作系统线程 + blocking I/O 与 Tokio 异步 I/O 差距可忽略,内存开销也相近。

  3. 协程的真正优势在更大规模时显现:当并发量达到数千时,线程模型会因栈内存(每线程约 2 MB 虚拟空间)和上下文切换开销开始吃力,协程的轻量级特性(每协程仅数 KB)将展现明显的伸缩性优势。