如何解决用lilos并行调用Rust结构体异步方法的可变借用问题?
解决Rust+Lilos嵌入式系统中异步并发的可变self借用冲突问题
问题背景
基于Rust和lilos crate开发小型嵌入式系统时,想要异步并发运行对象的两个方法,但由于两个方法都需要可变借用self,导致无法同时调用。需求是实现LED的两个功能:
- 异步闪烁LED
- 接收命令控制LED是否激活
当前代码如下:
use core::convert::Infallible; use embedded_hal::digital::v1::OutputPin; use futures::select_biased; use futures::FutureExt; use lilos; use lilos::exec::PeriodicGate; use thingbuf as conn; pub struct Led<'a> { pin: &'a mut dyn OutputPin, gate: PeriodicGate, recv_cmd: conn::mpsc::Receiver<bool>, send_cmd: conn::mpsc::Sender<bool>, active: bool, } impl<'a> Led<'a> { pub fn new(pin: &'a mut dyn OutputPin ) -> Self { let (send_cmd, recv_cmd) = conn::mpsc::channel::<bool>(10); let gate = PeriodicGate::from(lilos::time::Millis(500)); Self { pin, gate, send_cmd, recv_cmd, active: true, } } async fn cmd(&mut self, v: bool) { let _ = self.send_cmd.send(v).await; } async fn blink(&mut self) -> Infallible { loop { self.gate.next_time().await; let _ = self.pin.set_high(); self.gate.next_time().await; let _ = self.pin.set_low(); } } async fn on_cmd(&mut self) -> Infallible { loop { let cmd = self.recv_cmd.recv().await; if let Some(x) = cmd { self.active = x; } } } pub async fn run(&mut self) -> Infallible { select_biased!( _ = self.blink().fuse() => {}, _ = self.on_cmd().fuse() => {} ) } }
尝试过Arc、RefCell、Mutex但编译失败,仍存在借用冲突问题。
解决方案
由于lilos是单线程异步执行器,我们可以利用Rust的内部可变性(RefCell)安全地共享状态,同时拆分任务对self的依赖,避免同时借用整个可变self。
步骤1:重构结构体,分离共享状态
将需要跨任务共享的active标志用RefCell包裹,同时把命令发送器暴露给外部(移除内部冗余的cmd方法):
use core::cell::RefCell; use core::convert::Infallible; use embedded_hal::digital::v1::OutputPin; use futures::select_biased; use futures::FutureExt; use lilos; use lilos::exec::PeriodicGate; use thingbuf as conn; pub struct Led<'a> { pin: &'a mut dyn OutputPin, gate: PeriodicGate, recv_cmd: conn::mpsc::Receiver<bool>, active: RefCell<bool>, // 单线程下用RefCell实现安全的内部可变访问 } // 暴露给外部的命令发送器类型 pub type LedCmdSender = conn::mpsc::Sender<bool>;
步骤2:调整构造函数,返回发送器
构造函数返回Led实例和命令发送器,让外部可以直接发送控制命令:
impl<'a> Led<'a> { pub fn new(pin: &'a mut dyn OutputPin) -> (Self, LedCmdSender) { let (send_cmd, recv_cmd) = conn::mpsc::channel::<bool>(10); let gate = PeriodicGate::from(lilos::time::Millis(500)); let led = Self { pin, gate, recv_cmd, active: RefCell::new(true), }; (led, send_cmd) }
步骤3:修改任务方法,避免整体可变借用
让blink和on_cmd分别持有所需的部分引用,而非整个&mut self。单线程下RefCell的访问不会产生运行时panic:
// blink任务:仅依赖引脚、周期门和激活状态 async fn blink(pin: &mut dyn OutputPin, mut gate: PeriodicGate, active: &RefCell<bool>) -> Infallible { loop { gate.next_time().await; if *active.borrow() { let _ = pin.set_high(); } gate.next_time().await; if *active.borrow() { let _ = pin.set_low(); } } } // on_cmd任务:仅依赖命令接收器和激活状态 async fn on_cmd(mut recv_cmd: conn::mpsc::Receiver<bool>, active: &RefCell<bool>) -> Infallible { loop { if let Some(cmd) = recv_cmd.recv().await { *active.borrow_mut() = cmd; } } }
步骤4:修改run方法,拆分任务参数
将self的成员拆分后传递给两个任务,彻底避免同时借用整个可变self:
pub async fn run(mut self) -> Infallible { let active_ref = &self.active; select_biased!( _ = Self::blink(&mut self.pin, self.gate, active_ref).fuse() => {}, _ = Self::on_cmd(self.recv_cmd, active_ref).fuse() => {} ) } }
外部使用示例
// 初始化LED和命令发送器 let (mut led, cmd_sender) = Led::new(&mut led_pin); // 在其他任务中发送控制命令 lilos::exec::spawn(async move { // 3秒后关闭LED lilos::time::sleep(lilos::time::Millis(3000)).await; let _ = cmd_sender.send(false).await; }); // 运行LED主任务 led.run().await;
关键说明
- 单线程内部可变性:lilos是单线程执行器,
RefCell完全安全,无需使用多线程的Mutex,避免不必要的开销。 - 拆分任务依赖:通过将任务需要的参数单独传递,而非持有整个
self的可变引用,从根源上解决了借用冲突。 - 移除冗余逻辑:原代码中Led内部持有send/receiver是不必要的,将sender暴露给外部,符合单一职责原则。
内容的提问来源于stack exchange,提问作者Lieven
相关产品推荐
相关产品推荐

