Rust 异步 —— 让嵌入式编程更加简单
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_something(); break;
case 'B': do_something_else(); break;
default: break;
}
}
do_something();
}
在这个简单的例子中,逻辑非常清晰,但 CPU 的执行效率却很低,因为它会阻塞在串口的读操作上。更有经验的程序员可能会使用中断或操作系统来提高效率,但这需要小心处理多线程或中断带来的其他风险,同时代码的可读性也会下降。
如果使用 Rust 中的 Future 替代这个程序,则可以如下实现:
async fn loop() {
let ch = serial.read().await;
match ch {
'A' => do_something(),
'B' => do_something_else(),
_ => do_some(),
}
do_something();
}
在异步 Rust 代码中,依旧保持了串行编程的模式,但 CPU 或线程不会阻塞在 read()
操作上,而是在后台等待数据来临时自动唤醒,极大地提高了执行效率。
Future 的实现原理
在大多数等待任务完成的场景下,系统后台需要一个执行器(executor),通过事件唤醒 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<T> {
Ready(T),
Pending,
}
trait Future {
type Output;
fn poll(&mut self, cx: &Context) -> Poll<Self::Output>;
}
#[derive(Default)]
struct MyFuture {
count: u32,
}
impl Future for MyFuture {
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
match self.count {
3 => Poll::Ready(3),
_ => {
self.count += 1;
ctx.waker().wake();
Poll::Pending
}
}
}
}
fn run<F>(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();
println!("Output: {}", run(my_future)); // 输出:Output: 3
}
上述代码展示了一个简单的 Future 调度器 run
函数,它不断检查任务的状态,并通过 Waker
唤醒。最终的任务结果通过 Poll::Ready
返回。
使用 embassy 异步处理串口数据
Rust 的 embassy 提供了一个简单的异步嵌入式编程框架,使用它可以轻松处理嵌入式设备中的异步任务。以下是 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!");
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 进行异步编程几乎没有额外的成本。它不会生成多余的状态逻辑代码,同时代码的可读性也依旧很高。如果你有兴趣深入学习,可以阅读以下书籍: