Skip to content

Rust Tokio 完整工程:有界任务、保序与失败收拢 · 全部同源工程文件至来源 ​

返回分篇目录

前置:任务所有权。项目位于 examples/rust/p2-async-runtime,读取文件的每一行,真实计算 u64 checked sum;不需要账号、服务或端口。本次示例未执行,未运行 Cargo/Rust 编译、测试或格式化,也未安装依赖。未来读者可按 README 自跑核验;静态解析不能证明 Rust 类型正确或运行行为通过。

全部同源工程文件 ​

以下包含完整工程与 README,均从仓库源文件 include,不维护第二份代码。Rust、TOML、输入和 Markdown 显式标记语言,阅读当前页即可取得未来自跑所需内容。

Cargo.toml:

toml
[package]
name = "p2-async-runtime"
version = "0.1.0"
edition = "2021"
rust-version = "1.71"

[dependencies]
tokio = { version = "=1.48.0", features = ["rt-multi-thread", "macros", "sync"] }

src/lib.rs:

rust
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use tokio::task::JoinSet;

#[derive(Debug)]
pub enum Job {
    Sum(Vec<u64>),
    Reject,
    Panic,
}

#[derive(Debug, Default)]
pub struct Metrics {
    active: AtomicUsize,
    peak: AtomicUsize,
}
impl Metrics {
    pub fn active(&self) -> usize { self.active.load(Ordering::SeqCst) }
    pub fn peak(&self) -> usize { self.peak.load(Ordering::SeqCst) }
}
struct Active(Arc<Metrics>);
impl Active {
    fn enter(metrics: Arc<Metrics>) -> Self {
        let n = metrics.active.fetch_add(1, Ordering::SeqCst) + 1;
        metrics.peak.fetch_max(n, Ordering::SeqCst);
        Self(metrics)
    }
}
impl Drop for Active {
    fn drop(&mut self) { self.0.active.fetch_sub(1, Ordering::SeqCst); }
}

// Every line is one owned job. ERR deliberately becomes a task-level error.
pub fn parse(text: &str) -> Result<Vec<Job>, String> {
    text.lines().enumerate().map(|(i, line)| {
        if line == "ERR" { return Ok(Job::Reject); }
        let numbers = line.split_whitespace().map(|word| {
            word.parse::<u64>().map_err(|_| format!("line {}: invalid u64", i + 1))
        }).collect::<Result<Vec<_>, _>>()?;
        Ok(Job::Sum(numbers))
    }).collect()
}

async fn calculate(job: Job, metrics: Arc<Metrics>) -> Result<u64, String> {
    let _active = Active::enter(metrics);
    match job {
        Job::Reject => Err("rejected job".into()),
        Job::Panic => panic!("intentional task panic"),
        Job::Sum(numbers) => {
            let mut sum = 0_u64;
            for chunk in numbers.chunks(64) {
                for value in chunk {
                    sum = sum.checked_add(*value).ok_or("sum overflow")?;
                }
                // Bounded CPU work followed by a cooperative scheduling point.
                tokio::task::yield_now().await;
            }
            Ok(sum)
        }
    }
}

// The caller must await this function to completion to observe drain-on-error.
// Dropping this future drops JoinSet and requests abort, but cannot await cleanup.
pub async fn analyze(jobs: Vec<Job>, limit: usize, metrics: Arc<Metrics>)
    -> Result<Vec<u64>, String>
{
    if limit == 0 { return Err("limit must be positive".into()); }
    let mut results = vec![0; jobs.len()];
    let mut pending = jobs.into_iter().enumerate();
    let mut running = JoinSet::new();
    loop {
        while running.len() < limit {
            let Some((index, job)) = pending.next() else { break; };
            let metrics = Arc::clone(&metrics);
            running.spawn(async move { (index, calculate(job, metrics).await) });
        }
        let Some(completion) = running.join_next().await else { return Ok(results); };
        let error = match completion {
            Ok((index, Ok(value))) => { results[index] = value; continue; }
            Ok((_, Err(error))) => error,
            Err(error) => format!("task join failed: {error}"),
        };
        running.abort_all();
        // Abort requests do not establish that destructors have completed.
        while running.join_next().await.is_some() {}
        return Err(error);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn abort_is_observed_after_join() {
        let metrics = Arc::new(Metrics::default());
        let copy = Arc::clone(&metrics);
        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
        let mut tasks = JoinSet::new();
        tasks.spawn(async move {
            let _active = Active::enter(copy);
            ready_tx.send(()).unwrap();
            std::future::pending::<()>().await;
        });
        ready_rx.await.unwrap();
        assert_eq!(metrics.active(), 1);
        tasks.abort_all();
        let error = tasks.join_next().await.unwrap().unwrap_err();
        assert!(error.is_cancelled());
        assert_eq!(metrics.active(), 0);
        assert!(tasks.is_empty());
    }
}

src/main.rs:

rust
use std::{env, fs, process::ExitCode, sync::Arc};
use p2_async_runtime::{analyze, parse, Metrics};

fn run() -> Result<(), String> {
    let args: Vec<String> = env::args().skip(1).collect();
    if args.len() != 2 { return Err("usage: p2-async-runtime INPUT LIMIT".into()); }
    let limit = args[1].parse::<usize>().map_err(|_| "invalid limit")?;
    if limit == 0 { return Err("limit must be positive".into()); }
    // Synchronous file read is intentionally before entering the runtime.
    let text = fs::read_to_string(&args[0]).map_err(|e| format!("read input: {e}"))?;
    let jobs = parse(&text)?;
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2).build().map_err(|e| format!("runtime: {e}"))?;
    let values = runtime.block_on(analyze(jobs, limit, Arc::new(Metrics::default())))?;
    for value in values { println!("{value}"); }
    Ok(())
}
fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => { eprintln!("{error}"); ExitCode::FAILURE }
    }
}

tests/behavior.rs:

rust
use std::sync::Arc;
use p2_async_runtime::{analyze, parse, Job, Metrics};

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn bounded_and_ordered() {
    let metrics = Arc::new(Metrics::default());
    let jobs = vec![Job::Sum(vec![1; 4096]), Job::Sum(vec![7]), Job::Sum(vec![])];
    assert_eq!(analyze(jobs, 2, Arc::clone(&metrics)).await.unwrap(), vec![4096, 7, 0]);
    assert!((1..=2).contains(&metrics.peak()));
    assert_eq!(metrics.active(), 0);
}
#[tokio::test]
async fn rejects_and_drains() {
    let metrics = Arc::new(Metrics::default());
    let jobs = vec![Job::Sum(vec![1; 4096]), Job::Reject];
    assert_eq!(analyze(jobs, 2, Arc::clone(&metrics)).await.unwrap_err(), "rejected job");
    assert_eq!(metrics.active(), 0);
}
#[tokio::test]
async fn panic_and_overflow_are_errors() {
    for job in [Job::Panic, Job::Sum(vec![u64::MAX, 1])] {
        let metrics = Arc::new(Metrics::default());
        assert!(analyze(vec![job, Job::Sum(vec![1; 4096])], 2, Arc::clone(&metrics)).await.is_err());
        assert_eq!(metrics.active(), 0);
    }
}
#[tokio::test]
async fn empty_and_zero_limit() {
    assert_eq!(analyze(vec![], 1, Arc::new(Metrics::default())).await.unwrap(), Vec::<u64>::new());
    assert!(analyze(vec![], 0, Arc::new(Metrics::default())).await.is_err());
}
#[test]
fn parser_contract() {
    assert_eq!(parse("1 2\r\n\r\n3").unwrap().len(), 3);
    assert!(parse("-1").is_err());
    assert!(parse("18446744073709551616").is_err());
    assert!(matches!(parse("ERR").unwrap()[0], Job::Reject));
}

inputs/normal.txt:

text
1 2 3
7
0 4

inputs/reject.txt:

text
1 2
ERR
9

inputs/overflow.txt:

text
18446744073709551615 1

inputs/invalid.txt:

text
-1

DEPENDENCIES.md:

md
# 依赖与版本

本工程示例未执行,未安装依赖,未生成 Cargo.lock。Tokio 固定 =1.48.0(公开发布版本,不宣称最新版);edition 2021,声明的 rust-version 1.71 对齐该版 Tokio MSRV,不代表本工程已做最低版本编译。

启用 rt-multi-thread 提供多线程运行时并包含 rt;macros 提供测试属性;sync 提供取消测试中的 oneshot。没有网络、时间或 fs 驱动,不调用 enable_all。标准库提供文件读取、数值检查、原子计数。不依赖 tokio-util。

核对:<https://docs.rs/tokio/1.48.0/tokio/index.html>、<https://docs.rs/tokio/1.48.0/tokio/task/struct.JoinSet.html>。真正自跑前由读者准备可信 Rust 工具链和缓存依赖;首次正常 Cargo 解析会生成锁文件,应用应审阅并提交它。offline 不会替你下载缺失依赖。


固定tag的 [Tokio Cargo.toml](https://raw.githubusercontent.com/tokio-rs/tokio/tokio-1.48.0/tokio/Cargo.toml) 明确声明 rust-version = "1.71";传递依赖将来解析的版本还可能提高最低工具链要求,未生成锁文件时不能承诺整个依赖图的最低版本可复现。

未来首次取得依赖:有可信网络和已安装 Rust/Cargo 的读者可在此目录自行运行 `cargo fetch`,审阅解析到的 Cargo.lock 与依赖来源后,再按 README 的 `cargo test --offline`/`cargo run --offline` 核验。若已有完整缓存,可直接离线;若缓存不全,offline 报缺依赖,需补齐可信缓存或使用联网流程。这里提供的是未来步骤,本轮没有执行 fetch、安装、解析依赖图、编译或测试。

README.md:

md
# Tokio 有界求和实验

示例未执行。这里只做源码人工审阅、TOML/Markdown/JSON 纯解析,不声称编译或测试通过。

每个 UTF-8 输入行是一个任务,行内用空白分隔 u64;空行和为0,空文件输出为空。CRLF 支持。单行 ERR 是故意的任务失败;负数、非数字、超范围在提交任务前拒绝。求和溢出在任务中失败。每64项 yield 一次,实际计算输入而非 sleep 模拟成功。

## 未来读者自跑

准备依赖后在本目录执行(本次未执行):

```sh
cargo test --offline
cargo run --offline -- inputs/normal.txt 2
cargo run --offline -- inputs/reject.txt 2
cargo run --offline -- inputs/overflow.txt 2
cargo run --offline -- inputs/invalid.txt 2
cargo run --offline -- inputs/normal.txt 0
cargo run --offline -- missing.txt 2
```

手工预期:正常 stdout 依输入顺序为6、7、4,各占一行,退出0。完成顺序不可预测,输出顺序固定。reject 包含 rejected job,overflow 包含 sum overflow,invalid 包含 invalid u64,零上限包含 limit must be positive,缺文件包含 read input;失败 stderr 且退出1,没有部分 stdout。测试预期全部通过但尚未执行;panic 测试可能产生 panic hook 文本,这是被 JoinError 收拢的预期行为。

先读文件与解析,再进入运行时;填充不超过 limit 的 JoinSet;回收一个才补一个;失败停止补充、abort_all,再逐个 join 到空。已经完成的成功结果在整体失败时丢弃。Active 的 Drop 计数用于检查返回后无在途计算;oneshot 测试确认任务已进入后才取消,不依赖 sleep。测试不承诺两个任务必定同时活跃。

## 取消、内存与清理

仅正常 await analyze 到返回的调用满足等待清理协议;外部直接 drop analyze 只会触发 JoinSet Drop 请求取消,不能同步等析构完成。abort 在让出执行权后生效,不抢占当前同步计算;每个工作片段是有限的64次数值操作。已运行的 spawn_blocking 无法靠 abort 停止,本工程没有使用它。

无服务、端口、网络和输出文件。自跑生成 target 和 Cargo.lock;无需业务回滚,重跑时选择正常输入。不要删除输入源文件;可在确认目录后自行清理 target。输入与结果全部驻内存,任务数有界不等于内存恒定,也不是通用CPU线程池吞吐基准。取消不是撤销已经产生的外部副作用。

文件:Cargo.toml、DEPENDENCIES.md、src/lib.rs、src/main.rs、tests/behavior.rs、inputs 下四个输入和本 README。

练习 ​

① 改成最大值计算,空行怎样定义,哪些契约与测试要改?② 把结果直接在任务中 println,会破坏哪两条保证?③ 为使内存随 limit 而非文件大小变化,应同时改哪些层?

参考解法 ​

① 空行可返回 Option 或明确错误,需同步改结果类型、CLI、空行测试,调度器只作必要类型修改。② 完成先后不等于输入顺序,而且后续失败前已输出部分结果。③ 输入必须增量读取,提交容量与保序缓冲都要限制;只加入 Semaphore 而预读整文件仍是 O(n)。

来源 ​

2026-10-17核对 Tokio 1.48.0 特性、yield_now;其余语义链接见前两篇。源码静态审阅与输入手工推导不等同于实际 Cargo 执行。

继续阅读