摘要
- 爬虫程序开发与对比
本实验开发了四个Rust爬虫程序,从三个方面(多进程、多线程、异步协程)对网络爬虫进行了对比:
crawler_process:采用多进程模式,使用外部命令。特点是进程隔离性好,但内存开销极大,QPS和平均响应时间在三者中波动较大,整体性能相对较低。
crawler_thread:采用多线程模式。性能介于进程和协程之间,内存使用稳定在一个较低的水平。
crawler_async:采用基于Tokio的异步协程模式。内存使用虽起点略高,但整体增长平缓,占用最少,资源复用和调度效率最高。
结论:对于I/O密集型的网络爬虫应用,异步协程(crawler_async)在内存效率和资源开销方面具有显著优势。
- 为异步协程增加优先级调度
在用户态异步协程运行时中,扩展实现了完全公平调度算法(CFS) 来支持优先级调度。通过修改执行器代码(参考两个版本的priority_fut和priority100loc程序),使用BTreeMap模拟内核红黑树来维护任务的虚拟运行时间(vruntime),实现了优先级权重(nice值)的差异化调度。
验证测试:进行了多组不同优先级场景的长时间压力测试。结果证明,在运行时内不同任务的执行次数与其设置的nice值完全匹配,成功实现了优先级调度。测试也揭示,权重计算本身的复杂度会对全局任务执行总次数产生轻微影响,这是一个有趣的额外发现。
- 异步协程状态机分析
本任务深入研究了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程序,如下:
- crawler_async:这是一个基于Rust异步运行时Tokio开发的高性能并发网页爬虫程序。它实现了并行抓取多个网站HTML页面,并将其保存到本地文件系统的核心功能。使用Tokio作为异步运行时。通过在main函数上标注
#[tokio::main],程序启动了一个Tokio运行时,使得所有异步任务(async)能够高效执行。通过tokio::sync::mpsc(多生产者,单消费者)通道(channel) 实现跨任务的解耦式通信。爬虫任务将抓取到的网页内容(名称, HTML)发送到通道,由另一个异步任务接收并写入文件。 - crawler_thread:它是一个基于 Rust 标准库多线程(std::thread)实现的同步阻塞式网页爬虫。程序通过创建多个工作线程并行抓取网页,并利用生产者-消费者模型(使用 std::sync::mpsc 通道)将抓取到的网页内容与耗时统计分析逻辑解耦,最终将抓取的 HTML 内容保存至本地文件系统。同时,该程序在关键执行点集成了对进程内存使用的精细监控。
- curl_one:它是将爬虫每个请求的处理,作为一个独立的、支持命令行调用的单次网页抓取工具,由主进程按需启动,完成“发送请求-保存文件-返回指标”这一原子操作。编译成二进制后由父进程crawler_process并发调用。
- 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个测试程序在内存使用方面的差异:
- 多进程爬虫程序在孵化多进程时内存使用暴增,然后随着子进程退出,内存使用等比下降。
- 多线程和异步协程爬虫的内存使用是逐渐增长的。
- 异步协程与多线程爬虫相比,初始内存使用较多,反应了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 代码链接
二. 用户态协程增加优先级调度
2.1 内容与目标
- 目标:选择一种执行流机制,在其中扩展优先级支持。
- 首先为《rust语言圣经》中异步章节的异步执行器增加优先级调度机制。训练营上一阶段对CFS算法有详细讲解,初步将其移植到异步执行器中,代替原有的通道,形成对异步协程的优先级调度机制。
- 为《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)算法的异步任务运行时。该程序主要用于演示如何在用户态通过命令行参数控制多个并发任务的执行优先级,并统计不同优先级下任务的执行次数。
priority100loc程序是直接将arceos的CFS算法代码直接复制过来,放在项目main.rs的尾部,并做了一些修改,以适合用户态使用,以及调用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:任务3优先级为-18,其它任务优先级为0,优先级列表为:
[0, 0, -18, 0,0, 0]。 - 测试2:任务3优先级为+18,其它任务优先级为0,优先级列表为:
[0, 0, 18, 0,0, 0]。 - 测试3:所有任务优先级都为0,优先级列表为:
[0, 0, 0, 0,0, 0]。 - 测试4:任务的优先级逐渐下降,优先级列表为:
[-15, -10, -5, 0,5, 10]。 - 测试5:任务在每次执行时睡眠50微秒,所有任务优先级都为0,任务优先级列表为:
[0, 0, 0, 0,0, 0]。 - 测试6:所有任务优先级都为-19,任务优先级列表为:
[-19, -19, -19, -19,-19, -19]。 - 测试7:任务的优先级交错设置,优先级列表为:
[3, -2, 16, -8,0, 17]。 - 测试8:任务的优先级从19逐渐上升,优先级列表为:
[19, 18, 17, 16, 15, 14]。 - 测试9:任务的优先级从-19逐渐下降,优先级列表为:
[-19, -18, -17, -16, -15, -14]。 - 测试10:任务的优先级从-19和+19之间来回切换,优先级列表为:
[-19, 19, -19, 19, -19, 19]。 - 测试11:所有任务优先级都为-19,任务优先级列表为:
[-19, -19, -19, -19,-19, -19]。 - 测试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 结论
- 各个测试结果说明实际运行结果符合优先级的设定。
- 测试5说明:单个任务睡眠,但在调度器中仍计数运行时间,所以即便优先级相同,这一任务运行次数仍会下降。
- 从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 代码链接
三. 异步协程状态机分析
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文件方法
- 在项目Cargo.toml的
[profile.dev]节中增加调试选项
1 | [profile.dev] |
- 构建时增加dump-mir选项,生成的mir文件在目录mir_dump下
1 | rustup install nightly |
3.4 异步协程需要关注的文件
dump到的.mir文件非常多,与异步协程相关的主要是几个以下面字符串结尾的MIR文件:
- .coroutine_before.0.mir(通常完整文件名类似 crate_name::module::function::{closure#0}.coroutine_before.0.mir)代表了异步函数(或协程)在转换为状态机结构之前、但在初步 lowering 之后的中间表示。
- .coroutine_async_drop_expand.0.mir(或类似名称)的文件,代表了编译器在处理异步代码时一个非常特定且关键的阶段:异步 Drop 展开(Async Drop Expansion)。
- coroutine_drop_proxy_async.0.mir(或类似名称如 coroutine_drop_glue)代表了编译器为异步协程(Async Coroutine)生成的析构代理(Drop Proxy/Glue)的 MIR。
- coroutine_drop.0.mir(通常完整路径包含函数名,如
fn_name::{closure#0}.coroutine_drop.0.mir)是 Rust 编译器为异步协程(Async Coroutine)生成的 标准析构函数(Drop Implementation) 的 MIR 表示。异步 Future 的“清理脚本”。 - coroutine_post-transform.0.mir(或类似名称,如
.coroutine.after_lowering)代表了异步协程(Async Coroutine)在完成 核心状态机转换Coroutine Lowering 之后、但在进行大规模优化之前的 MIR 状态。这是你看到的最终形态的异步状态机代码。在此之前,代码还是一个包含yield的普通函数;在此之后,它变成了一个真正的、基于枚举的状态机结构。 - coroutine_pre-elab.0.mir(通常完整名称包含
elaborate_drops或类似字样,如fn_name.coroutine_pre-elaborate-drops.0.mir)代表了异步协程在 析构细节展开Drop Elaboration 之前的 MIR 状态。 - 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可以静态获得状态机状态并大体构建执行流。
CoroutineLayout::field_tys包含所有跨.await生存的变量,即状态机中存储的变量。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 | Unresumed(0) - 初始状态,尚未开始执行 |
状态机字段分析 跨状态存活字段
1 | _s0: std::string::String - 函数参数 name |
各状态特有字段 Suspend0 (状态3)
1 | _s2: &str - 目录路径字符串 "websiteshtml/" |
Suspend1 (状态4)
1 | _s5: tokio::fs::File::create 的 Future 对象 |
Suspend2 (状态5)
1 | _s4: tokio::fs::File - 已打开的文件句柄 |
Suspend3 (状态6)
1 | _s4: tokio::fs::File - 文件句柄(继续使用) |
执行流程分析
初始化和日志输出
函数开始时输出日志:”开始进入函数save_response, 名称:” 设置目录路径为 “websiteshtml/“
状态转换流程 阶段1:创建目录 (Suspend0 → Suspend1)
调用 tokio::fs::create_dir_all(“websiteshtml/“) 输出日志:”函数save_response, 名称: 第一次.await之前” 等待目录创建完成 输出日志:”函数save_response, 名称: 第一次.await之后”
阶段2:创建文件 (Suspend1 → Suspend2)
1 | 构建文件名:name + ".html" |
阶段3:写入数据 (Suspend2 → Suspend3)
1 | 调用 file.write_all(body.as_bytes()) |
阶段4:刷新缓冲区 (Suspend3 → Returned)
1 | 调用 file.flush() |
内存布局优化 存储冲突分析
从 storage_conflicts 可以看出编译器进行了内存优化:
1 | _s0 和 _s1 与所有其他字段冲突,说明它们在整个生命周期中都存在 |
字段生命周期管理
编译器通过 variant_fields 精确控制每个状态需要保存的字段:
1 | Suspend0: 保存 _s0, _s1, _s2, _s3 |
错误处理机制
状态机包含完整的错误处理路径:
1 | 每个异步操作都有对应的 Result 类型返回 |
性能特点
1 | 零成本抽象:状态机大小由最大变体决定(Suspend2 和 Suspend3 最大) |
调试信息
MIR 中包含了完整的调试信息:
1 | 每个字段都有对应的源码位置(span) |
这个状态机展示了 Rust 异步编程的核心优势:将高级的 async/await 语法转换为高效、确定性的状态机,同时保持内存安全和零成本抽象。
3.7 结论
通过分析MIR文件,可以静态获得异步协程函数状态机状态及状态改变的完整过程,并能从中构建出异步协程的执行流。
可以用CoroutineLayout::field_tys和CoroutineLayout::variant_fields来复核AI分析的输出结果是否正确。