Futures在Rust中用于异步编程,类似JavaScript的promise的原理,两者都是async/await语句的基础,用户可用它们用串行编程的方式实现异步的功能。
Futures在标准的std和嵌入式的nostd环境都有支持,使用方式一致,在std环境中,比较出名的有Tokio实现了异步的平台,在嵌入式领域中,embassy也提供了高效的异步平台。
到底什么是 Future?
简单的说,Future用于表示一些异步计算的值,也就是说无法在当前得出最终的结果,但由于串行的程序中,又需要该计算的结果用于后续的操作。举个例子,在嵌入式中,通常处理串口接收和处理数据时,采用串行编程的方式如下:
void loop() {
char ch;
if (ch == serial.read())
switch ch {
case 'A': do_someting(); break;
case 'B': do_someting_else(); break;
...
default: break;
}
}
do_someting();
}
在这个简单的例程中,可以很容易看出处理的逻辑,但是CPU的执行效率却很低,CPU或进程会阻塞在串口的读接口中。也许更加有经验的程序员会用中断或操作系统来实现这个功能。但是需要加倍小心多线程或中断引发的其他风险,同时代码的可阅读性会降低,需要去了解操作系统和信号量等全局变量。如果使用Rust中Future来替代该程序,则可简单如下:
async fn loop() {
let ch = serial.read().await;
match ch {
'A' => {
do_someting();
}
'B' => {
do_someting_else();
}
_ => {
do_some();
}
}
do_someting();
}
在异步的Rust代码中,同样保持了串行的编程模式,但CPU或线程不会在read()停留等待可读数据,而是在后台数据来临时自动唤醒。这样提高了运行效率。
Future的实现原理
在大多数需要等待结束的任务中,系统后台需要一个执行器,通过唤醒Future的事件来重新激活await语句,简单得说,需要执行器对该任务实现任务和激活机制的抽象,该抽象无需反复去查询事件信号,而任务是被动让激活信号重新唤醒。Future的简单模型如下:
use std::cell::RefCell;
thread_local!(static NOTIFY: RefCell<bool> = RefCell::new(true));
struct Context<'a> {
waker: &'a Waker,
}
impl<'a> Context<'a> {
fn from_waker(waker: &'a Waker) -> Self {
Context { waker }
}
fn waker(&self) -> &'a Waker {
&self.waker
}
}
struct Waker;
impl Waker {
fn wake(&self) {
NOTIFY.with(|f| *f.borrow_mut() = true)
}
}
enum Poll {
Ready(T),
Pending,
}
trait Future {
type Output;
fn poll(&mut self, cx: &Context) -> Poll;
}
#[derive(Default)]
struct MyFuture {
count: u32,
}
impl Future for MyFuture {
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll {
match self.count {
3 => Poll::Ready(3),
_ => {
self.count += 1;
ctx.waker().wake();
Poll::Pending
}
}
}
}
fn run(mut f: F) -> F::Output
where
F: Future,
{
NOTIFY.with(|n| loop {
if *n.borrow() {
*n.borrow_mut() = false;
let ctx = Context::from_waker(&Waker);
if let Poll::Ready(val) = f.poll(&ctx) {
return val;
}
}
})
}
fn main() {
let my_future = MyFuture::default();
/// 将输出:Output: 3
println!("Output: {}", run(my_future));
}
如上所示,run函数带有一个Future属性,可理解为调度器。在NOTIFY的信号中重新激活任务,然后返回执行结果,给Poll::Ready带出。通常NOTIFY的loop闭包至少会执行一次,第一次是首次进入poll任务,如果poll任务后没有结束,将会在Waker信号被激活时重新唤起执行poll的任务。如果没有唤醒事件,则调度器不会主动去执行Poll。Context用来传递任务的上下文,可用来保存当前任务的事件信号等。Poll则是一个简单的枚举,Ready代表任务可以结束后的结果,将带回结果返回值,Pending则代表当前任务并未结束,则继续睡眠。Futuretrait 则是任务的抽象,任何只要实现Future``trait,都可使用在异步编程中,即使用async/awit。在该例中根据原理实现了一个最简单的Future的调度器,以及一个实现了Future的结构体对象MyFuture。也许看起来要实现异步需要这么多代码,似乎有点复杂!别担心,这些Rust都已经为您提供了,你需要写的也就是main函数的内容,甚至更加简单。下面展示embassy使用异步的方式处理串口数据。
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
let mut config = uarte::Config::default();
config.parity = uarte::Parity::EXCLUDED;
config.baudrate = uarte::Baudrate::BAUD115200;
let mut uart = uarte::Uarte::new(p.UARTE0, Irqs, p.P0_08, p.P0_06, config);
info!("uarte initialized!");
// Message must be in SRAM
let mut buf = [0; 8];
buf.copy_from_slice(b"Hello!\r\n");
unwrap!(uart.write(&buf).await);
info!("wrote hello in uart!");
loop {
info!("reading...");
unwrap!(uart.read(&mut buf).await);
info!("writing...");
unwrap!(uart.write(&buf).await);
}
}
最后
在Rust中使用Future是零成本抽象的,即不会生成多余的状态逻辑代码,同时CPU的运行也不会造成负荷,同时代码的可阅读性依旧很强。如果有兴趣可以深入阅读以下书籍:
Asynchronous Programming in Rust:https://rust-lang.github.io/async-book/02_execution/02_future.html Async programming in Rust with async-std:https://book.async.rs/concepts/futures Async Rust:https://www.oreilly.com/library/view/async-rust/9781098149086/