Rust 异步 —— 让嵌入式编程更加简单

FuturesRust中用于异步编程,类似JavaScriptpromise的原理,两者都是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;
            ...

            defaultbreak;
        }
    }
    do_someting();
}

在这个简单的例程中,可以很容易看出处理的逻辑,但是CPU的执行效率却很低,CPU或进程会阻塞在串口的读接口中。也许更加有经验的程序员会用中断或操作系统来实现这个功能。但是需要加倍小心多线程或中断引发的其他风险,同时代码的可阅读性会降低,需要去了解操作系统和信号量等全局变量。如果使用RustFuture来替代该程序,则可简单如下:

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带出。通常NOTIFYloop闭包至少会执行一次,第一次是首次进入poll任务,如果poll任务后没有结束,将会在Waker信号被激活时重新唤起执行poll的任务。如果没有唤醒事件,则调度器不会主动去执行PollContext用来传递任务的上下文,可用来保存当前任务的事件信号等。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 = [08];
    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/

请使用浏览器的分享功能分享到微信等