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 的用武之地。