2.2 Tasks
既然我们知道Futures是什么,我们就要运行它们!
在async-std中,tasks模块负责这个的。最简单的方法是使用block_on函数:
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的运行时执行读取文件的代码。接下来让我们一个接一个从里到外分析下。
async {
let result = read_file("data.csv").await;
match result {
Ok(s) => println ! ("{}", s),
Err(e) => println !("Error reading file: {:?}", e)
}
};
这是一个异步块。异步块是调用异步函数所必需的,它将指示编译器包含所有相关的指令。
在Rust中,所有块都返回一个值,而异步块就是返回一个Future类型的值。
但让我们来看看有趣的部分:
task::spawn(async { });
spawn方法需要一个Future类型并开始在一个任务上运行它并返回一个JoinHandle类型值。
Rust的Futures有时被称为懒惰的Futures。你需要一些东西来运行它们。
为了运行一个Future,可能需要一些额外的记录状态来记录一些信息,例如它是运行的还是完成的,它放在内存中的位置以及当前的状态。
这个记录状态是在一个任务中抽象出来的。
任务类似于线程,但有一些细微的区别:它将由程序而不是操作系统内核调度,如果遇到需要等待的点,程序本身将负责再次唤醒它。
我们稍后再谈。异步任务也可以有名称和ID,就像线程一样。
现在,只要知道一旦生成了一个任务,它将继续在后台运行就足够了。
JoinHandle本身就是一个future,它将在任务运行到结束时完成。
与线程和join函数非常相似,我们现在可以调用句柄上的block_on来阻塞程序(或调用线程,具体来说)并等待它完成。
2.2.1 async_std中的任务
async_std中的Tasks是核心抽象之一。就像Rust的线程一样,它们提供了一些超越原始概念的实用功能。
任务与运行时有关系,但它们本身是独立的。sync_std的tasks具有许多有用的属性:
它们被分配到一个单独的内存
所有任务都有一个backchannel(后台通道),允许它们通过JoinHandle将结果和错误传播到调用者
它们为调试提供了有用的元数据
它们支持任务本地存储
async_stds task API可以为您处理后台运行时的任务配置管理,而不依赖于显式启动的运行时。
2.2.2 阻塞
假设任务并发运行,可能是基于在一个共享线程上来运行的(一个线程运行多个协程,一个任务表示一个协程)。
这意味着阻塞操作系统线程的操作,例如来自Rust的std库的std::thread::sleep或io操作函数,将停止执行在此共享线程的所有任务。其他库(如数据库驱动程序)也有类似的行为。
请注意,阻塞当前线程本身并不是一种不良行为,只是与async-std的并发模型不太匹配。事实上,永远不要这样做:
fn main() {
task::block_on(async {
// this is std::fs, which blocks,
//这里使用了标准库的std::fs方法来读文件,如果一个文件很大的话,将会阻塞很长时间
std::fs::read_to_string("test_file");
})
}
如果要混合类似操作,请考虑将此类阻塞操作放在单独的线程上。
2.2.3 错误和恐慌
任务通过正常模式报告错误:如果它们是错误的,那么它们的输出应该是Result<T,E>类型。
在恐慌的情况下,行为的不同取决于是否有一个合理的部分来解决恐慌。否则,程序将中止。
实际上,这意味着block-on将恐慌传播到blocking组件:
fn main() {
task::block_on(async {
panic!("test");
});
}
thread 'async-task-driver' panicked at 'test', examples/panic.rs:8:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
上面的信息展示了,协程任务发生恐慌时,没有在任务代码进行处理恐慌时,这个恐慌传播到了main主线程.
在恐慌生成的任务时将中止:
task::spawn(async {
panic!("test");
});
task::block_on(async {
task::sleep(Duration::from_millis(10000)).await;
})
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)
一开始这可能看起来很奇怪,但另一个选择是默默地忽略衍生任务中的恐慌。
当前的行为可以通过捕获派生任务中的恐慌并对自定义行为作出反应来改变。这为用户提供了恐慌处理策略的选择。
2.2.4 结论
async_std附带了一个有用的任务类型,它与类似td::thread的API一起工作。它以结构化和明确的方式涵盖了错误和恐慌行为。
任务是独立的并发单元,有时它们需要通信。That's where Streams come in.