0%

2026春季训练营总结报告-基于协程异步机制的操作系统驱动-郝光煜

摘要

  1. 爬虫程序开发与对比

本实验开发了四个Rust爬虫程序,从三个方面(多进程、多线程、异步协程)对网络爬虫进行了对比:

crawler_process‌:采用多进程模式,使用外部命令。特点是进程隔离性好,但内存开销极大,QPS和平均响应时间在三者中波动较大,整体性能相对较低。
crawler_thread‌:采用多线程模式。性能介于进程和协程之间,内存使用稳定在一个较低的水平。
crawler_async‌:采用基于Tokio的异步协程模式。内存使用虽起点略高,但整体增长平缓,占用最少,资源复用和调度效率最高。

结论‌:对于I/O密集型的网络爬虫应用,异步协程(crawler_async)在内存效率和资源开销方面具有显著优势。

  1. 为异步协程增加优先级调度

在用户态异步协程运行时中,扩展实现了‌完全公平调度算法(CFS)‌ 来支持优先级调度。通过修改执行器代码(参考两个版本的priority_fut和priority100loc程序),使用BTreeMap模拟内核红黑树来维护任务的虚拟运行时间(vruntime),实现了优先级权重(nice值)的差异化调度。

验证测试‌:进行了多组不同优先级场景的长时间压力测试。结果证明,在运行时内不同任务的执行次数与其设置的nice值完全匹配,成功实现了优先级调度。测试也揭示,权重计算本身的复杂度会对全局任务执行总次数产生轻微影响,这是一个有趣的额外发现。

  1. 异步协程状态机分析

本任务深入研究了Rust异步/await语法糖背后的实现原理。通过生成和分析编译器MIR中间表示(特别是*.coroutine_post-transform.0.mir文件),直观地剖析了异步函数如何被编译器转换为一个‌基于枚举(enum)的有限状态机‌。该状态机精确管理了协程在挂起(Suspend)、运行中(包含Unresumed)以及最终完成/恐慌(Returned/Panicked)等不同状态,并智能地在各状态间复用和存储变量的内存,以实现高效、零成本的异步抽象。
总体结论

此次实践系统性地覆盖了异步编程的三个核心层面:

实际应用性能对比‌:确认了异步模型在高并发网络I/O任务中的性能和资源优势。
调度机制扩展‌:成功在用户态实现了基于CFS算法的优先级调度,加深了对操作系统调度原理在异步模型中的融合理解。
底层机制洞察‌:通过分析编译器生成的MIR,揭示了Rust异步协程的状态机本质,将高层的async/await映射为确定的、可分析的底层执行流。

本次实验不仅提升了异步编程技巧,更重要的是建立了从应用性能、运行时调度到底层实现机制的全链路知识体系。

周报链接

异步操作系统训练营开营与目标对齐交流会

异步项目第一周周报(2026-05-11~2026-05-17

异步项目第二周周报(2026-05-18~2026-05-24)

异步项目第三周周报(2026-05-25~2025-05-31)

异步项目第四周周报(2026-06-01~2026-06-07)

爬虫程序执行状态分析

一. 爬虫程序开发与对比

1.1 内容与目标

分别使用进程,线程,协程编写网络爬虫程序,对列表文件中的各个高校网站的首页进行爬取,然后比较它们之间的差异。

1.2 程序描述

项目包括4个rust程序,如下:

  1. crawler_async:这是一个基于Rust异步运行时Tokio开发的‌高性能并发网页爬虫程序‌。它实现了并行抓取多个网站HTML页面,并将其保存到本地文件系统的核心功能。使用Tokio作为异步运行时。通过在main函数上标注#[tokio::main],程序启动了一个Tokio运行时,使得所有异步任务(async)能够高效执行。通过tokio::sync::mpsc(多生产者,单消费者)‌通道(channel)‌ 实现跨任务的解耦式通信。爬虫任务将抓取到的网页内容(名称, HTML)发送到通道,由另一个异步任务接收并写入文件。
  2. crawler_thread:它是一个‌基于 Rust 标准库多线程(std::thread)实现的同步阻塞式网页爬虫‌。程序通过创建多个工作线程并行抓取网页,并利用生产者-消费者模型(使用 std::sync::mpsc 通道)将抓取到的网页内容与耗时统计分析逻辑解耦,最终将抓取的 HTML 内容保存至本地文件系统。同时,该程序在关键执行点集成了对进程内存使用的精细监控。
  3. curl_one:它是将爬虫每个请求的处理,作为一个‌独立的、支持命令行调用的单次网页抓取工具‌,由主进程按需启动,完成“发送请求-保存文件-返回指标”这一原子操作。编译成二进制后由父进程crawler_process并发调用。
  4. crawler_process:这是一个‌基于多进程(子进程)并发机制的网页爬虫程序‌,核心逻辑是通过生成多个系统进程(调用外部命令 curl_one)来实现并行网页抓取,然后汇总统计性能数据。使用 std::thread::spawn 为标准库的多线程,每个线程负责调用一个外部进程(./curl_one)执行单次爬取任务,实现并发。并发度由网站列表长度决定。线程作为任务的发起者与守护者,负责创建子进程、监控其状态。实际工作由子进程执行的外部可执行文件 curl_one 承担。

1.3 测试内容

1.3.1 测试方法

在网络连通的情况下执行3个测试程序(crawler_async、crawler_thread、crawler_process),收集每个对外请求处理的延时,每个请求执行时的内存使用,请求处理的QPS,进行分析。
3个程序的请求都是并发进行,在请求成功返回并处理完毕后进行统计,所以下面测试结果中的请求标号是请求返回顺序的序号,而不是地址文件university-websites.txt中网站排序号。

1.3.2 测试环境

  • 硬件平台:笔记本电脑(CPU:Intel(R) Core(TM) i7-4810MQ CPU @ 2.80GHz, MEM:16GB)上的WSL虚拟机。
  • 操作系统:WSL虚拟机内安装ubuntu24.04
  • 测量轮次:每种情况测试5轮。

1.4 测试结果

1.4.1 多进程爬虫crawler_process

成功返回的结果计数 第一次测试延时(毫秒) 第一次测试内存使用(MB) 第二次测试延时(毫秒) 第二次测试内存使用(MB) 第三次测试延时(毫秒) 第三次测试内存使用(MB) 第四次测试延时(毫秒) 第四次测试内存使用(MB) 第五次测试延时(毫秒) 第五次测试内存使用(MB)
1 709 1269 548 1232 1092 1228 1209 1229 1209 1230
2 784 1239 795 1159 1202 1196 1303 1196 1227 1195
3 888 1200 979 1125 1364 1162 1474 1161 1429 1124
4 1005 1158 1274 1055 1664 1055 1537 1124 1625 1053
5 1120 1086 1404 1045 1818 1028 1709 1051 1833 1023
6 1365 1015 1441 1045 2082 1006 2027 987 2091 977
7 1347 978 1514 980 2129 977 2362 1029 2138 935
8 1419 940 1695 965 2148 941 2373 989 2254 917
9 1544 919 1716 922 2246 912 2383 948 2299 880
10 1653 887 1754 885 2246 912 2404 901 2304 880
11 1694 846 1774 846 2287 828 2428 858 2340 798
12 1843 777 1704 804 2320 790 2440 814 2503 784
13 1960 750 1762 778 2334 747 2472 739 2539 742
14 1998 714 1804 718 2376 710 2857 706 2748 707
15 2116 670 1823 675 2385 667 2868 662 2792 665
16 2126 632 1839 632 2408 623 2879 620 2957 626
17 2163 589 1811 590 2448 580 2892 577 2980 583
18 2312 547 1857 544 2700 544 3310 538 3282 544
19 2611 506 2012 502 2768 499 3393 496 3388 503
20 2772 465 2532 461 2832 457 3503 459 3502 463
21 2909 425 3275 422 3090 419 3508 417 3523 420
22 3136 380 3671 380 3097 378 4492 373 3690 379
23 6253 343 4273 337 3661 335 6063 333 4698 338
24 6289 299 6014 294 4138 293 6903 289 7118 305
25 7200 262 6855 252 4243 252 9664 249 7126 263
26 7252 218 7712 210 6439 208 11631 208 7147 220
27 7316 175 11784 118 7421 167 11671 166 7150 177
28 7390 134 12923 91 11402 126 13605 88 7158 130
29 9178 88 19858 63 11403 127 13758 61 8362 88
30 24726 32 21575 32 15157 34 14773 34
process 统计 QPS 最高内存使用(MB) 平均延时(秒)
第一次测试 0.95 1269 3.12
第二次测试 1.21 1232 4.44
第三次测试 1.38 1228 3.98
第四次测试 1.97 1229 4.81
第五次测试 2.02 1230 3.94
平均值 1.51 1238 4.06

1.4.2 多线程爬虫crawler_thread

成功返回的结果计数 第一次测试延时(毫秒) 第一次测试内存使用(MB) 第二次测试延时(毫秒) 第二次测试内存使用(MB) 第三次测试延时(毫秒) 第三次测试内存使用(MB) 第四次测试延时(毫秒) 第四次测试内存使用(MB) 第五次测试延时(毫秒) 第五次测试内存使用(MB)
1 767 13 425 13 210 13 267 13 385 13
2 1562 14 753 14 463 14 573 14 841 14
3 1619 14 866 15 524 15 826 17 1103 15
4 2421 15 974 17 768 17 826 17 1491 15
5 2608 16 974 17 772 17 873 17 1511 15
6 2651 16 977 17 832 18 936 19 1533 16
7 3012 18 1061 18 878 18 948 19 1830 19
8 3181 19 1069 19 916 19 956 20 1841 19
9 3181 19 1083 19 952 20 977 20 1857 20
10 3195 19 1209 20 955 20 1060 21 1857 20
11 3214 20 1227 20 972 21 1159 22 1976 21
12 3253 21 1276 21 1027 22 1316 23 2003 22
13 3258 21 1416 22 1098 24 1353 23 2003 22
14 3446 21 1416 22 1116 24 1443 24 2238 22
15 3457 22 1513 22 1158 25 1609 24 2576 23
16 3664 23 1786 23 1254 27 1636 25 2693 24
17 3786 24 2011 24 1269 27 1699 25 2715 24
18 4339 25 3142 25 1271 27 1907 26 2735 25
19 4340 25 3481 26 1358 28 2259 27 2830 25
20 4569 26 3644 26 1360 28 2586 28 3060 26
21 4958 26 5661 28 1742 28 2588 28 3113 27
22 5255 27 5661 28 1760 29 5153 28 3203 27
23 5508 27 5663 28 2299 30 6141 29 3387 28
24 5564 28 5685 28 2303 30 6161 29 4287 29
25 7604 29 5833 29 2635 30 7153 30 4820 29
26 9667 30 6691 30 3779 31 7168 30 5121 30
27 9672 30 6697 30 6101 31 7701 31 5128 30
28 11705 30 8720 31 7227 32 8270 31 7159 31
29 16045 30 9791 31 9443 32 9341 32 8062 31
30 11839 31 10418 33 20912 31 8418 32
thread channel统计 QPS 最高内存使用(MB) 平均延时(秒)
第一次测试 1.80 30 4.74
第二次测试 2.53 31 3.42
第三次测试 2.88 33 2.23
第四次测试 1.43 32 3.53
第五次测试 3.56 32 3.06
平均值 2.44 31.60 3.39

1.4.3 异步协程爬虫crawler_async

成功返回的结果计数 第一次测试延时(毫秒) 第一次测试内存使用(MB) 第二次测试延时(毫秒) 第二次测试内存使用(MB) 第三次测试延时(毫秒) 第三次测试内存使用(MB) 第四次测试延时(毫秒) 第四次测试内存使用(MB) 第五次测试延时(毫秒) 第五次测试内存使用(MB)
1 672 19 503 19 310 20 327 19 590 19
2 1382 20 575 20 357 20 451 20 639 20
3 3409 20 674 21 483 21 663 20 1136 22
4 4681 21 867 21 524 21 682 21 1174 23
5 4930 21 869 21 599 21 842 22 1217 24
6 5152 21 1015 22 711 23 920 23 1223 24
7 5730 22 1105 23 825 24 948 24 1226 24
8 5789 23 1176 23 831 24 953 24 1267 25
9 5804 23 1186 24 844 24 956 24 1275 25
10 6361 23 1189 24 910 25 1012 25 1276 25
11 6398 24 1194 24 950 26 1013 25 1398 25
12 6517 24 1395 25 998 26 1041 26 1535 26
13 6674 25 1403 26 1084 26 1072 26 1539 26
14 6969 25 1430 26 1125 27 1176 27 1549 26
15 6974 25 1467 26 1195 27 1212 27 2241 27
16 7190 26 1490 26 1742 27 1240 27 2609 27
17 7201 26 1580 27 1868 27 1257 27 2634 28
18 7451 26 1619 27 1915 27 1308 27 2634 28
19 7521 27 1672 27 1925 27 1858 28 2680 28
20 7727 27 1845 27 1964 27 1886 28 2993 28
21 7754 27 1979 28 2589 28 2059 28 3019 28
22 7768 27 2231 28 2992 28 2097 28 3349 29
23 7895 27 2947 28 3148 28 2451 28 3354 29
24 7898 27 2971 28 3290 28 2782 28 5922 29
25 8567 28 3360 28 5916 29 2923 28 5933 29
26 10666 28 6140 28 5917 29 4379 28 7036 29
27 11692 28 6327 29 5917 29 5332 28 7212 29
28 11860 28 6711 29 7987 29 6383 29 9319 29
29 11869 28 7170 29 9010 29 10620 29 10164 29
30 12838 28 7198 29 10027 30 11600 29 11243 29
31 14126 29 37596 29 15391 29 14742 29 15404 29
async channel统计 QPS 最高内存使用(MB) 平均延时(秒)
第一次测试 2.19 29 7.34
第二次测试 0.82 29 3.51
第三次测试 2.01 30 3.01
第四次测试 2.10 29 2.78
第五次测试 2.01 29 3.70
平均值 1.83 29.20 4.07

1.5 结论

从上面测试结果可以清楚看到3个测试程序在内存使用方面的差异:

  1. 多进程爬虫程序在孵化多进程时内存使用暴增,然后随着子进程退出,内存使用等比下降。
  2. 多线程和异步协程爬虫的内存使用是逐渐增长的。
  3. 异步协程与多线程爬虫相比,初始内存使用较多,反应了tokio异步协程库代码本身占用的内存。但异步协程内存使用增长比多线程平缓,这反应了无栈协程比多线程节省了栈的内存使用。

1.5.1 多进程爬虫crawler_process

优点:
高并发潜力‌:由于每个下载任务都在独立的系统进程中运行,相互隔离,稳定性较好。尤其当部分任务异常(如网络超时)时,对其他任务影响非常小。
简洁可靠‌:程序框架与标准库依赖,无需复杂异步编程。
I/O隔离‌:网络请求与可能的写入操作分散在不同子进程,减少了主进程中大内存占用的风险。
计算与I/O分离‌:主线程仅承担任务分发和进程管理,计算密集型的工作由外部进程处理。

缺点:
资源开销大‌:每次请求启动一个新进程的消耗远大于在进程内复用网络连接或线程执行异步I/O操作,特别是当任务量巨大(如上千级网站)时,可能会占用大量内存,甚至导致系统性能下降。
性能瓶颈‌:进程间切换与内核态调度的开销可能比用户态的异步并发高,尤其频繁启动、结束进程可能成为瓶颈。
外部依赖‌:程序功能依赖于 ./curl_one 命令的外部文件。该程序需要事先编译好并确保在主程序的同级目录下。

1.5.2 多线程爬虫crawler_thread

并发能力‌: 性能上应该介于进程模型(开销最大)和异步模型(开销最小)之间。

I/O密集型任务的效率‌:与异步/并发框架(例如 ‌Tokio‌)相比,thread::spawn 对于 I/O 密集型任务仍然存在显著的开销。每个线程都会阻塞等待网络响应,当并发线程数(网站数目)非常大时(成百上千级),将会因线程切换和上下文开销而增加 CPU 负载,可能降低整体性能。

线程数量的可控性‌:程序没有采用‌线程池‌模式,而是为每个网站创建一个线程。当网站数量很多时,创建的大量线程可能导致系统内核调度上的瓶颈。合理的线程池模式(如使用 rayon 库)能够更好地控制系统开销。

1.5.3 异步协程爬虫crawler_async

与多进程爬虫和多线程爬虫相比,使用内存最少,切换开销最小。爬虫程序主要受网络抖动影响,性能差异并不明显,但在内存开销方面可以看到异步协程有明显优势。

1.6 代码链接

crawler_async代码

crawler_thread代码

crawler_process代码

curl_one代码

二. 用户态协程增加优先级调度

2.1 内容与目标

  1. 目标:选择一种执行流机制,在其中扩展优先级支持。
  2. 首先为《rust语言圣经》中异步章节的异步执行器增加优先级调度机制。训练营上一阶段对CFS算法有详细讲解,初步将其移植到异步执行器中,代替原有的通道,形成对异步协程的优先级调度机制。
  3. 为《100行无栈协程》中的异步执行器增加优先级调度机制。将CFS算法移植到《100行无栈协程》中的异步执行器中,实现对异步协程的的优先级调度。

2.2 程序描述

2.2.1 priority_fut

这是一个‌基于自定义完全公平调度器(CFS)实现的可设置优先级的 Rust 异步任务执行器程序‌。程序的核心目标不是执行具体的网络爬虫或计算任务,而是‌研究、演示和验证自定义调度策略在异步编程中的应用‌,特别是在多任务场景下,通过完全公平调度算法(CFS)和优先级控制,观察不同优先级任务的执行顺序如何被影响。

本程序调度的核心算法——‌完全公平调度(Completely Fair Scheduler, CFS)‌ 的 Rust 实现。它通常通过记录和比较任务的虚拟运行时间(vruntime)来决定下一个调度的任务,优先级会影响 vruntime 的增长速度,使得高优先级任务获得更多执行时间。
这里的 CFScheduler 负责管理一个 CFSTask 的任务队列,提供如 pick_next_task、put_prev_task、set_priority、task_tick 等方法,来控制任务的出入队列、优先级设置和时间片调整。

理解算法的生效方式‌:为了使 CFS 的优先级效果在短期测试(8秒定时)中肉眼可见,程序需要大量快速的 “poll + 返回 Pending” 循环来快速消耗 CPU 时间并推进行调度器的虚拟运行时间计算(例如 spawn 方法后 1024 次调用 task_tick)。

模拟长期调度环境‌:CFS 的设计初衷是在较长时间尺度(比如秒级、毫秒级的真实调度)上维护公平性,但简短的纯软件测试难以体现该算法的权重累积效果。因此开发者通过手动干预让 CPU 快速地进行计算与调度决策,以便在一个演示程序中直观地看到优先级差异。

然而,值得注意的是,由于大量人工干预调度计算,这不属于可直接生产环境使用的调度库,而是‌自定义异步调度机制的原型研究与演示‌。

2.2.2 priority100loc

代码实现了一个基于完全公平调度器(CFS)算法的异步任务运行时。该程序主要用于演示如何在用户态通过命令行参数控制多个并发任务的执行优先级,并统计不同优先级下任务的执行次数。

ArceOS官方的CFS算法地址链接

priority100loc程序是直接将arceos的CFS算法代码直接复制过来,放在项目main.rs的尾部,并做了一些修改,以适合用户态使用,以及调用CFS的私有方法:

priority100loc的CFS算法链接

程序构建了一个简易的操作系统级调度器模型,主要包含以下核心组件:

  • CFS 调度器:模拟了 Linux 内核的完全公平调度器(CFS)逻辑。它使用 BTreeMap 作为红黑树的替代,根据任务的虚拟运行时间(vruntime)来决定下一个执行的任务,确保高优先级(权重高)的任务获得更多的 CPU 时间。
  • 异步运行时:实现了一个简易的 Executor,能够接收异步闭包(Future)并执行。它通过一个空的轮询循环(run 方法)来驱动任务。
  • 优先级控制:支持通过命令行参数设置 6 个任务的优先级(nice 值),范围为 -20 到 19。

程序使用 clap 库解析输入:

  • --prio:接受 6 个空格分隔的整数,分别对应 6 个任务的优先级。
  • -d / --delay_task_one:一个布尔标志,用于决定是否让任务 1 引入微小的延迟(50 微秒),这会显著影响其调度频率。

程序运行 30 秒后停止,并输出每个任务内部原子计数器的值,用于直观展示优先级对任务执行频率的影响。

2.3 测试内容

2.3.1 测试方法

由于priority_fut进行较多人工干预,可以熟悉异步协程及CFS算法原理,但人工干预较多,说明效果相对低,这里主要针对priority100loc进行测试。
运行priority100loc, 通过使用不同的命令行参数为6个任务设置各种优先级,或让任务1睡眠50微秒,通过30秒内各个任务运行的次数观察优先级的执行情况。因为优先级越高(–prio的值越小),任务得到运行的机会越高,运行的次数也就应该比低优先级的任务高。也就是高优先级执行次数多,低优先级执行次数少
每一场景重复进行5次测试,各个场景的测试交错进行,收集6个任务的执行次数进行分析。
任务号从1开始。
所有任务都是同质的,只有当启用--delay-task-one时,任务1会每次执行会增加50微秒的睡眠。

2.3.2 测试环境

  • 硬件平台:笔记本电脑(CPU:Intel(R) Core(TM) i7-4810MQ CPU @ 2.80GHz, MEM:16GB)上的WSL虚拟机。
  • 操作系统:WSL虚拟机内安装的ubuntu24.04
  • 测量轮次:每一场景重复测试5次。

    2.3.3 测试场景

  1. 测试1:任务3优先级为-18,其它任务优先级为0,优先级列表为: [0, 0, -18, 0,0, 0]
  2. 测试2:任务3优先级为+18,其它任务优先级为0,优先级列表为: [0, 0, 18, 0,0, 0]
  3. 测试3:所有任务优先级都为0,优先级列表为: [0, 0, 0, 0,0, 0]
  4. 测试4:任务的优先级逐渐下降,优先级列表为: [-15, -10, -5, 0,5, 10]
  5. 测试5:任务在每次执行时睡眠50微秒,所有任务优先级都为0,任务优先级列表为: [0, 0, 0, 0,0, 0]
  6. 测试6:所有任务优先级都为-19,任务优先级列表为: [-19, -19, -19, -19,-19, -19]
  7. 测试7:任务的优先级交错设置,优先级列表为: [3, -2, 16, -8,0, 17]
  8. 测试8:任务的优先级从19逐渐上升,优先级列表为: [19, 18, 17, 16, 15, 14]
  9. 测试9:任务的优先级从-19逐渐下降,优先级列表为: [-19, -18, -17, -16, -15, -14]
  10. 测试10:任务的优先级从-19和+19之间来回切换,优先级列表为: [-19, 19, -19, 19, -19, 19]
  11. 测试11:所有任务优先级都为-19,任务优先级列表为: [-19, -19, -19, -19,-19, -19]
  12. 测试12:任务1优先级为-16,其它为-5,任务优先级列表为: [-16, -5, -5, -5,-5, -5]

    2.4 测试结果

    2.4.1 测试1

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 524426 495667 563711 523726 510096 523525
2 537517 527176 582233 512081 539515 539704
3 32874928 33498971 34807078 33877244 33740747 33759794
4 495608 490358 552831 499765 519064 511525
5 492120 512088 552502 514952 542661 522865
6 542960 479696 576577 479768 528296 521459
总执行次数 35467559 36003956 37634932 36407536 36380379 36378872
运行命令 ./priority100loc –prio 0 0 -18 0 0 0

2.4.2 测试2

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 7495654 7411949 7505272 7654233 7713202 7556062
2 6887367 7503910 7539407 7849526 7652740 7486590
3 117721 111570 119324 131796 125602 121203
4 7438860 7550351 7531448 7614096 7545250 7536001
5 7287715 7626784 7609210 7530652 7549708 7520814
6 7507020 7528093 7046191 7794306 7676440 7510410
总执行次数 36734337 37732657 37350852 38574609 38262942 37731079
运行命令 ./priority100loc –prio 0 0 18 0 0 0

2.4.3 测试3

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 6338273 6346417 6161578 6220578 6317908 6276951
2 6182495 6352568 6307718 6257312 6214059 6262830
3 6352438 6258802 6192278 6452642 6322180 6315668
4 6466035 6351864 6227901 6403985 6463463 6382650
5 6505075 6235031 6337990 6558159 6439099 6415071
6 6309518 6350785 6306983 6451381 6277962 6339326
总执行次数 38153834 37895467 37534448 38344057 38034671 37992495
运行命令 ./priority100loc

2.4.2 测试4

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 24807902 24519290 24504660 25056134 24887297 24755057
2 7273175 7378454 7346925 7252905 7352835 7320859
3 2320327 2319150 2254512 2407879 2295268 2319427
4 766136 731679 760759 762991 735433 751400
5 234500 215286 233259 236636 233538 230644
6 71329 71775 64473 68911 57431 66784
总执行次数 35473369 35235634 35164588 35785456 35561802 35444170
运行命令 ./priority100loc –prio -15 -10 -5 0 5 10

2.4.5 测试5

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 796 792 871 789 819 813
2 7625964 7676110 7663107 7824023 7676540 7693149
3 7675830 7708657 7534059 7618943 7608375 7629173
4 7706226 7744701 7694302 7751725 7610052 7701401
5 7720577 7476638 7683668 7737662 7555287 7634766
6 7785577 7759579 7637143 7771900 7619092 7714658
总执行次数 38514970 38366477 38213150 38705042 38070165 38373961
运行命令 ./priority100loc –delay-task-one

2.4.6 测试6

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 6128323 5986477 6065969 6198079 5886064 6052982
2 6124712 6052704 6070059 6115089 5967384 6065990
3 6122994 5972958 6039993 6123538 6023912 6056679
4 6309840 6123930 6021390 6070900 5879856 6081183
5 6313224 6132050 6189299 6082905 6055913 6154678
6 5640508 6071286 6043216 6013185 5931930 5940025
总执行次数 36639601 36339405 36429926 36603696 35745059 36351537
运行命令 ./priority100loc –prio -19 -19 -19 -19 -19 -19

2.4.7 测试7

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 2044583 1984462 1886239 1991459 1982130 1977775
2 6379222 6315280 6416028 6468360 6473837 6410545
3 91546 118726 86618 113121 95582 101119
4 26041482 25786572 26292617 26132147 26741641 26198892
5 4203781 3959375 4221686 4321252 4200423 4181303
6 74461 78767 75413 95555 85478 81935
总执行次数 38835075 38243182 38978601 39121894 39579091 38951568
运行命令 ./priority100loc –prio 3 -2 16 -8 0 17

2.4.8 测试8

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 3411393 3413169 3350819 3349387 3300040 3364962
2 4005013 4071409 4066249 4077452 4024824 4048989
3 5191674 5263767 5174058 5092429 5125709 5169527
4 6742951 6450670 6645637 6647389 6549705 6607270
5 8026476 8102475 8209471 8276922 8331433 8189355
6 10254394 10356769 10284942 10311101 10363921 10314225
总执行次数 37631901 37658259 37731176 37754680 37695632 37694330
运行命令 ./priority100loc –prio 19 18 17 16 15 14

2.4.9 测试9

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 9737352 9714999 9839490 9714043 9522830 9705743
2 7417436 7505936 7550560 7537489 7488182 7499921
3 6172818 6153440 6290409 6122554 6088414 6165527
4 4851346 4799726 4874082 4671378 4802577 4799822
5 3740114 3849336 3743193 3740868 3700960 3754894
6 3006319 3166688 3100174 3002087 2998361 3054726
总执行次数 34925385 35190125 35397908 34788419 34601324 34980632
运行命令 ./priority100loc –prio -19 -18 -17 -16 -15 -14

2.4.10 测试10

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 11794818 11925685 11940234 11994454 11948733 11920785
2 4426 1455 6290 3057 2411 3528
3 11974606 12054148 11919065 11933364 12103960 11997029
4 1561 2198 873 1632 244 1302
5 11758884 12215031 11971916 12077625 11840660 11972823
6 2447 2691 1710 4726 4405 3196
总执行次数 35536742 36201208 35840088 36014858 35900413 35898662
运行命令 ./priority100loc –prio -19 19 -19 19 -19 19

2.4.11 测试11

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 6548657 6600643 6538788 6469139 6454396 6522325
2 6674471 6517480 6516750 6489857 6593603 6558432
3 6454296 6596074 6525975 6495802 6574747 6529379
4 6522890 6597248 6493671 6455683 6570316 6527962
5 6636044 6538444 6567737 6514242 6516768 6554647
6 6445091 6454171 6526455 6399061 6579185 6480793
总执行次数 39281449 39304060 39169376 38823784 39289015 39173536
运行命令 ./priority100loc –prio 19 19 19 19 19 19

2.4.12 测试12

任务号 第1次执行 第2次执行 第3次执行 第4次执行 第5次执行 平均值
1 27397743 27526233 27636269 27586946 27891687 27607776
2 2147835 2112519 2163235 2084309 2117882 2125156
3 2184851 2217172 2155626 2103135 2191366 2170430
4 2086911 2100050 2161729 2139182 2186779 2134930
5 2124831 2149577 2053953 2183482 2134972 2129363
6 2106303 2221812 2159033 2126517 2128585 2148450
总执行次数 38048474 38327363 38329845 38223571 38651271 38316105
运行命令 ./priority100loc –prio -16 -5 -5 -5 -5 -5

2.4.13 每次测试执行的任务总数统计

测试条件(30秒6个任务运行总次数) 结果为5次统计平均值
任务3优先级为-18,其它为0 36378872
任务3优先级为18,其它为0。 37731079
所有任务优先级为0。 37992495
任务优先级为[-15,-10,-5,0,5,10]。 35444170
所有任务优先级为0,任务1每次睡眠50微秒。 38373961
所有任务优先级为-19。 36351537
任务优先级为[3,-2,16,-8,0,17] 38951569
所有任务优先级为+19 39173537
任务1优先级为-16,其它为-5 38316105
任务优先级为[-19,19,-19,19,-19,19] 35898662
任务优先级为[-19,-18,-17,-16,-15,-14] 34980632.2
任务优先级为[19,18,17,16,15,14] 37694329.6

2.5 结论

  1. 各个测试结果说明实际运行结果符合优先级的设定。
  2. 测试5说明:单个任务睡眠,但在调度器中仍计数运行时间,所以即便优先级相同,这一任务运行次数仍会下降。
  3. 从2.4.13节《每次测试执行的任务总数统计》中可以看出,各种场景的任务总数是有稍许差异,这与CFS算法的机制有关。在CFS算法中,优先级数字(-20~+19)每个都对应一个权重值,0对应权重1024,优先级数值越大权重值越小,优先级数值越小权重值越大。-20的权重是88761,-19是71755;而+19的权重值是15,+18的权重值是18。CFS算法在每次计算权重时,需要计算以权重值为除数的除法,CPU计算5位数的除法和2位数的除法是有细微差异的,5位数除法的计算复杂度稍高计算耗时也稍长。在几千万次的循环过程中,这一细微差异被放大。这就造成,如果测试优先级包含较大负数(例如:-19)时,执行任务的总数会下降,因为优先级运算消耗了更多CPU时间

2.6 代码链接

priority_fut代码地址

priority100loc代码地址

三. 异步协程状态机分析

3.1 内容与目标

分析异步协程状态机的内部机制和执行流转换。

3.2 Rust程序MIR文件介绍

MIR‌是‌Mid-level Intermediate Representation‌的缩写,翻译为‌中级中间表示‌,是Rust编译器内部用于表示程序代码的一种中间格式,是Rust编译流程里非常关键的一步。

Rust编译源代码到最终机器码的大致流程是:
源代码(Rust语法) → 抽象语法树(AST) → HIR(高级中间表示) → MIR(中级中间表示) → LLVM IR/机器码

Rust异步状态机,本质就是在MIR阶段生成的:async fn,源代码会被编译器拆分为:一个状态枚举(每个await点对应一个状态)+ 一个实现了Future的结构体。 这个拆分和生成过程,就是在MIR阶段完成的,因此我们能通过查看MIR,直接看到编译器生成的异步状态机结构。

3.3 获得MIR文件方法

  1. 在项目Cargo.toml的[profile.dev]节中增加调试选项
1
2
3
4
[profile.dev]
debug = 2          # 对应 -g (生成完整调试信息)
opt-level = 0      # 关闭优化,防止变量被优化掉
lto = false        # 关闭链接时优化,保留更多符号信息
  1. 构建时增加dump-mir选项,生成的mir文件在目录mir_dump下
1
2
rustup install nightly
RUSTFLAGS="-Z dump-mir=all" cargo +nightly build

3.4 异步协程需要关注的文件

dump到的.mir文件非常多,与异步协程相关的主要是几个以下面字符串结尾的MIR文件:

  1. ‌.coroutine_before.0.mir‌(通常完整文件名类似 crate_name::module::function::{closure#0}.coroutine_before.0.mir)代表了异步函数(或协程)在‌转换为状态机结构之前、但在初步 lowering 之后‌的中间表示。
  2. ‌.coroutine_async_drop_expand.0.mir‌(或类似名称)的文件,代表了编译器在处理异步代码时一个非常特定且关键的阶段:‌异步 Drop 展开(Async Drop Expansion)‌。
  3. coroutine_drop_proxy_async.0.mir‌(或类似名称如 coroutine_drop_glue)代表了编译器为异步协程(Async Coroutine)生成的‌析构代理(Drop Proxy/Glue)‌的 MIR。
  4. coroutine_drop.0.mir‌(通常完整路径包含函数名,如 fn_name::{closure#0}.coroutine_drop.0.mir)是 Rust 编译器为异步协程(Async Coroutine)生成的‌ 标准析构函数(Drop Implementation) 的 MIR 表示。异步 Future 的“清理脚本”。
  5. coroutine_post-transform.0.mir(或类似名称,如 .coroutine.after_lowering)代表了异步协程(Async Coroutine)在完成‌ 核心状态机转换Coroutine Lowering‌ 之后、但在进行大规模优化之前的 MIR 状态。这是你看到的‌最终形态的异步状态机代码。在此之前,代码还是一个包含 yield 的普通函数;在此之后,它变成了一个真正的、基于枚举的状态机结构。
  6. coroutine_pre-elab.0.mir‌(通常完整名称包含 elaborate_drops 或类似字样,如 fn_name.coroutine_pre-elaborate-drops.0.mir)代表了异步协程在‌ 析构细节展开Drop Elaboration‌ 之前的 MIR 状态。
  7. coroutine_resume.0.mir‌(或类似名称,如 fn_name::{closure#0}.coroutine_resume.0.mir)代表了异步协程(Async Coroutine)中专门用于‌处理“从挂起状态恢复执行”逻辑‌的 MIR 片段。

3.5 *.coroutine_post-transform.0.mir文件分析

分析核心状态机转换,主要在这一文件中。从field_tys和variant_fields可以静态获得状态机状态并大体构建执行流。

  1. CoroutineLayout::field_tys 包含所有跨.await生存的变量,即状态机中存储的变量。
  2. CoroutineLayout::variant_fields 是状态机转换的各个阶段需要存储field_tys中的哪些变量,例如:
    • Unresumed(0): [], - 初始状态,尚未开始执行
    • Returned (1): [], - 协程已正常完成
    • Panicked (2): [], - 协程因 panic 终止
    • Suspend0 (3): [_s0, _s1, _s2, _s3], - 第一个挂起点,存储s0, s1, s2, s3。
    • Suspend1 (4): [_s0, _s1, _s5, _s8], - 第二个挂起点,存储s0, s1, s5, s8。
    • Suspend2 (5): [_s0, _s1, _s4, _s6, _s8], - 第三个挂起点,存储s0, s1, s4, s6, s8。
    • Suspend3 (6): [_s0, _s1, _s4, _s7, _s8], - 第四个挂起点,存储s0, s1, s4, s7, s8。

3.6 使用大模型分析.mir文件

.mir文件很繁琐,通读需用很多时间。在了解其结构的基础上,可以将.mir文件直接上传到大模型,AI可以很快梳理出状态机状态变化和协程执行流。

下面是大模型对爬虫的异步函数save_response的分析结果:

该异步函数被编译成一个包含 ‌7 个变体(Variants)‌ 的状态机枚举,对应不同的执行阶段: 状态变体定义

1
2
3
4
5
6
7
‌Unresumed(0)‌ - 初始状态,尚未开始执行
‌Returned(1)‌ - 协程已正常完成
‌Panicked(2)‌ - 协程因 panic 终止
‌Suspend0(3)‌ - 第一个挂起点(等待目录创建)
‌Suspend1(4)‌ - 第二个挂起点(等待文件创建)
‌Suspend2(5)‌ - 第三个挂起点(等待数据写入)
‌Suspend3(6)‌ - 第四个挂起点(等待缓冲区刷新)

状态机字段分析 跨状态存活字段

1
2
3
‌_s0‌: std::string::String - 函数参数 name
‌_s1‌: std::string::String - 函数参数 body
‌_s8‌: bool - 内部标志位,用于跟踪某些状态

各状态特有字段 Suspend0 (状态3)

1
2
‌_s2‌: &str - 目录路径字符串 "websiteshtml/"
‌_s3‌: tokio::fs::create_dir_all 的 Future 对象

Suspend1 (状态4)

1
‌_s5‌: tokio::fs::File::create 的 Future 对象

Suspend2 (状态5)

1
2
‌_s4‌: tokio::fs::File - 已打开的文件句柄
‌_s6‌: tokio::io::util::write_all::WriteAll - 写入操作的 Future

Suspend3 (状态6)

1
2
‌_s4‌: tokio::fs::File - 文件句柄(继续使用)
‌_s7‌: tokio::io::util::flush::Flush - 刷新操作的 Future

执行流程分析

  1. 初始化和日志输出

    函数开始时输出日志:”开始进入函数save_response, 名称:” 设置目录路径为 “websiteshtml/“

  2. 状态转换流程 阶段1:创建目录 (Suspend0 → Suspend1)

    调用 tokio::fs::create_dir_all(“websiteshtml/“) 输出日志:”函数save_response, 名称: 第一次.await之前” 等待目录创建完成 输出日志:”函数save_response, 名称: 第一次.await之后”

阶段2:创建文件 (Suspend1 → Suspend2)

1
2
3
4
5
构建文件名:name + ".html"
调用 tokio::fs::File::create(file_name)
输出日志:"函数save_response, 名称: 第二次.await之前"
等待文件创建完成
输出日志:"函数save_response, 名称: 第二次.await之后,第三次之前"

阶段3:写入数据 (Suspend2 → Suspend3)

1
2
3
调用 file.write_all(body.as_bytes())
输出日志:"函数save_response, 名称: 第三次.await之后,第四次之前"
等待数据写入完成

阶段4:刷新缓冲区 (Suspend3 → Returned)

1
2
3
4
调用 file.flush()
输出日志:"函数save_response, 名称: 第四次.await之后"
等待缓冲区刷新完成
清理资源并返回结果

内存布局优化 存储冲突分析

从 storage_conflicts 可以看出编译器进行了内存优化:

1
2
3
4
5
6
7
_s0 和 _s1 与所有其他字段冲突,说明它们在整个生命周期中都存在
_s2(目录路径)只在 Suspend0 阶段使用
_s3(目录创建 Future)只在 Suspend0 阶段使用
_s4(文件句柄)在 Suspend2 和 Suspend3 阶段使用
_s5(文件创建 Future)只在 Suspend1 阶段使用
_s6(写入 Future)只在 Suspend2 阶段使用
_s7(刷新 Future)只在 Suspend3 阶段使用

字段生命周期管理

编译器通过 variant_fields 精确控制每个状态需要保存的字段:

1
2
3
4
‌Suspend0‌: 保存 _s0, _s1, _s2, _s3
‌Suspend1‌: 保存 _s0, _s1, _s5, _s8
‌Suspend2‌: 保存 _s0, _s1, _s4, _s6, _s8
‌Suspend3‌: 保存 _s0, _s1, _s4, _s7, _s8

错误处理机制

状态机包含完整的错误处理路径:

1
2
3
4
每个异步操作都有对应的 Result 类型返回
使用 Try trait 的 branch 方法进行错误传播
通过 FromResidual trait 将 std::io::Error 转换为 anyhow::Error
每个挂起点都有对应的清理代码(cleanup basic blocks)

性能特点

1
2
3
4
‌零成本抽象‌:状态机大小由最大变体决定(Suspend2 和 Suspend3 最大)
‌内存复用‌:编译器通过存储冲突分析优化内存使用
‌最小化移动‌:字符串等数据只在必要时移动
‌精确的析构‌:每个状态结束时正确清理资源

调试信息

MIR 中包含了完整的调试信息:

1
2
3
每个字段都有对应的源码位置(span)
变量名映射(如 debug name => ((_1 as variant#6).0: std::string::String))
作用域信息(scope[0], scope[1] 等)

这个状态机展示了 Rust 异步编程的核心优势:将高级的 async/await 语法转换为高效、确定性的状态机,同时保持内存安全和零成本抽象。

3.7 结论

通过分析MIR文件,可以静态获得异步协程函数状态机状态及状态改变的完整过程,并能从中构建出异步协程的执行流。

可以用CoroutineLayout::field_tysCoroutineLayout::variant_fields来复核AI分析的输出结果是否正确。

3.8 代码链接

crawler_async代码

PPT链接

2026春训练营总结报告PPT