2.2 任务

原文链接: https://book.async.rs/concepts/tasks.html

既然我们知道了 Future 是什么,我们就想运行它们!

在 async-std 中,task 模块负责这件事。最简单的方式是使用 block_on 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# extern crate async_std;
use async_std::{fs::File, io, prelude::*, task};

async fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

fn main() {
    let reader_task = task::spawn(async {
        let result = read_file("data.csv").await;
        match result {
            Ok(s) => println!("{}", s),
            Err(e) => println!("Error reading file: {:?}", e)
        }
    });
    println!("Started task!");
    task::block_on(reader_task);
    println!("Stopped task!");
}

这要求内置于 async_std 的运行时执行读取文件的代码。不过,让我们从内到外逐一来看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# extern crate async_std;
# use async_std::{fs::File, io, prelude::*, task};
#
# async fn read_file(path: &str) -> io::Result<String> {
#     let mut file = File::open(path).await?;
#     let mut contents = String::new();
#     file.read_to_string(&mut contents).await?;
#     Ok(contents)
# }
#
async {
    let result = read_file("data.csv").await;
    match result {
        Ok(s) => println!("{}", s),
        Err(e) => println!("Error reading file: {:?}", e)
    }
};

这是一个 async 块。异步块是调用 async 函数所必需的,会指示编译器包含所有相关指令以做到这一点。在 Rust 中,所有块都返回值,async 块恰好返回 Future 类型的值。

但让我们来看有趣的部分:

1
2
3
# extern crate async_std;
# use async_std::task;
task::spawn(async { });

spawn 接受一个 Future 并在 Task 上开始运行它。它返回一个 JoinHandle。Rust 中的 Future 有时称为冷 Future。你需要某种东西来启动它们的运行。要运行 Future,可能需要一些额外的簿记,例如它是否正在运行或已完成、在内存中的位置以及当前状态。这部分簿记被抽象在 Task 中。

Task 类似于 Thread,有一些细微差别:它由程序调度,而不是由操作系统内核调度;如果它遇到需要等待的点,程序本身负责再次唤醒它。我们稍后会稍微讨论这一点。async_std 任务也可以像线程一样有名称和 ID。

目前,知道一旦你 spawn 了一个任务,它就会在后台继续运行就足够了。JoinHandle 本身是一个 future,一旦 Task 运行到结束就会完成。与 thread 和 join 函数类似,我们现在可以在句柄上调用 block_on 来阻塞程序(更准确地说,是调用线程)并等待其完成。

async_std 中的任务

async_std 中的任务是核心抽象之一。与 Rust 的 thread 类似,它们在原始概念之上提供了一些实用功能。Task 与运行时有关联,但它们本身是独立的。async_std 任务具有许多理想属性:

  • 它们在单次分配中分配
  • 所有任务都有一个回传通道,允许它们通过 JoinHandle 将结果和错误传播给生成任务
  • 它们携带用于调试的有用元数据
  • 它们支持任务局部存储

async_std 的任务 API 为你处理支持运行时的设置和拆卸,不依赖显式启动运行时。

阻塞

Task 假定并发运行,可能通过共享执行线程来实现。这意味着阻塞操作系统线程的操作,例如 std::thread::sleep 或 Rust std 库中的 I/O 函数,将_停止共享该线程的所有任务的执行_。其他库(如数据库驱动)也有类似行为。请注意,_阻塞当前线程_本身并不是不良行为,只是与 async-std 的并发执行模型不太契合。本质上,永远不要这样做:

1
2
3
4
5
6
7
8
# extern crate async_std;
# use async_std::task;
fn main() {
    task::block_on(async {
        // 这是 std::fs,会阻塞
        std::fs::read_to_string("test_file");
    })
}

如果你想混合不同类型的操作,考虑将此类阻塞操作放在单独的 thread 上。

错误与 panic

任务通过常规模式报告错误:如果它们可能失败,其 Output 应为 Result<T,E> 类型。

在 panic 的情况下,行为取决于是否有合理的部分来处理 panic。如果没有,程序会_中止_。

实践中,这意味着 block_on 将 panic 传播给阻塞组件:

1
2
3
4
5
6
7
# extern crate async_std;
# use async_std::task;
fn main() {
    task::block_on(async {
        panic!("test");
    });
}
1
2
thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

而在已 spawn 的任务中 panic 会导致中止:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# extern crate async_std;
# use async_std::task;
# use std::time::Duration;
task::spawn(async {
    panic!("test");
});

task::block_on(async {
    task::sleep(Duration::from_millis(10000)).await;
})
1
2
3
thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Aborted (core dumped)

起初这可能显得奇怪,但另一种选择是静默忽略已 spawn 任务中的 panic。当前行为可以通过在已 spawn 的任务中捕获 panic 并以自定义行为响应来更改。这给了用户选择 panic 处理策略的自由。

结论

async_std 附带一个有用的 Task 类型,API 与 std::thread 类似。它以结构化和定义明确的方式涵盖错误和 panic 行为。

任务是独立的并发单元,有时它们需要通信。这就是 Stream 的用武之地。

最后修改 August 23, 2026: 更新 (499855b16)