Rust Tokio上下文调用方法返回Future时的生命周期错误排查
问题背景
正在学习Rust,用异步重写WebSocket订阅器(网络IO密集场景大量使用async)。设计思路:用common_task处理WebSocket订阅等通用逻辑,Worker只需实现消息处理逻辑common_task_step,同时Worker可通过get_additional_tasks返回额外Future,与common_task一同在run_worker中await。但出现E0597生命周期错误,提示*worker生命周期不足,尽管tasks已在worker销毁前完成await仍报错。尝试过Box<dyn Future<Output = () + 'a>>、Arc<dyn Future = ()>等包装方式,问题依旧。
错误信息
error[E0597]: `*worker` does not live long enough --> src/main.rs:67:21 | 65 | async fn run_worker<'a>(worker: Box<dyn LongWorker<'a>>) { | ------ lifetime `'1` appears in the type of `worker` 66 | // This complains about worker being dropped while borrowed 67 | let mut tasks = worker.get_additional_tasks().await; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | borrowed value does not live long enough | argument requires that `*worker` is borrowed for `'1` ... 75 | } | - `*worker` dropped here while still borrowed For more information about this error, try `rustc --explain E0597`.
完整代码
use std::time::SystemTime; use futures::{FutureExt, future::LocalBoxFuture}; // 0.3.27 use async_trait::async_trait; // 0.1.66 use tokio::time::{interval, sleep, Duration}; #[async_trait] trait LongWorker<'a> { async fn get_additional_tasks(&'a self) -> Vec<LocalBoxFuture<'a, ()>>; async fn common_task_step(&'a self, some_arg: u64) -> (); } // Nothing is done on tasks struct NoOpWorker {} #[async_trait] impl<'a> LongWorker<'a> for NoOpWorker { async fn get_additional_tasks(&'a self) -> Vec<LocalBoxFuture<'a, ()>> { vec![] } async fn common_task_step(&'a self, some_arg: u64) { println!("Noop Got arg {}", some_arg); } } // There are some tasks to be done struct SleepWorker {} impl SleepWorker { async fn sleep(&self, millis: u64) -> () { sleep(Duration::from_millis(millis)).await; println!("Slept for {}ms", millis); } } #[async_trait] impl<'a> LongWorker<'a> for SleepWorker { async fn get_additional_tasks(&'a self) -> Vec<LocalBoxFuture<'a, ()>> { vec![ self.sleep(500).boxed_local(), self.sleep(1500).boxed_local(), self.sleep(2500).boxed_local(), self.sleep(3500).boxed_local(), ] } async fn common_task_step(&'a self, some_arg: u64) { println!("Sleeper Got arg {}", some_arg); } } async fn common_task<'a>(worker: &'a dyn LongWorker<'a>) { // IRL this would subscribe to websocket + handle shutdown signals etc. let mut interval = interval(Duration::from_millis(100)); for _ in 1 .. 5 { interval.tick().await; worker.common_task_step( SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 ).await; } } async fn run_worker<'a>(worker: Box<dyn LongWorker<'a>>) { // This complains about worker being dropped while borrowed let mut tasks = worker.get_additional_tasks().await; // This works // let mut tasks = vec![]; tasks.push(common_task(worker.as_ref()).boxed_local()); // But future ends here (thus it is not borrowed anymore) futures::future::join_all(tasks).await; // And worker is dropped here } #[tokio::main(worker_threads = 5)] async fn main() { let noop_worker = Box::new(NoOpWorker {}); let sleep_worker = Box::new(SleepWorker {}); run_worker(noop_worker).await; run_worker(sleep_worker).await; }
问题根源
原LongWorker trait将生命周期参数'a同时绑定到&'a self和返回的LocalBoxFuture<'a, ()>,强制Future与self的引用生命周期完全一致。但异步场景中,编译器无法追踪Future的实际执行流,会默认认为Future的生命周期需要覆盖整个run_worker函数,而worker在函数末尾销毁,导致生命周期不匹配报错。
解决方法
方法1:用Arc共享Worker所有权
让Future持有Worker的Arc实例,而非引用,彻底解决生命周期绑定问题:
use std::sync::Arc; use std::time::SystemTime; use futures::{FutureExt, future::LocalBoxFuture}; use async_trait::async_trait; use tokio::time::{interval, sleep, Duration}; #[async_trait] trait LongWorker { async fn get_additional_tasks(self: Arc<Self>) -> Vec<LocalBoxFuture<'static, ()>>; async fn common_task_step(&self, some_arg: u64); } struct NoOpWorker {} #[async_trait] impl LongWorker for NoOpWorker { async fn get_additional_tasks(self: Arc<Self>) -> Vec<LocalBoxFuture<'static, ()>> { vec![] } async fn common_task_step(&self, some_arg: u64) { println!("Noop Got arg {}", some_arg); } } struct SleepWorker {} impl SleepWorker { async fn sleep(&self, millis: u64) { sleep(Duration::from_millis(millis)).await; println!("Slept for {}ms", millis); } } #[async_trait] impl LongWorker for SleepWorker { async fn get_additional_tasks(self: Arc<Self>) -> Vec<LocalBoxFuture<'static, ()>> { vec![ async move { self.clone().sleep(500).await }.boxed_local(), async move { self.clone().sleep(1500).await }.boxed_local(), async move { self.clone().sleep(2500).await }.boxed_local(), async move { self.sleep(3500).await }.boxed_local(), ] } async fn common_task_step(&self, some_arg: u64) { println!("Sleeper Got arg {}", some_arg); } } async fn common_task(worker: Arc<dyn LongWorker>) { let mut interval = interval(Duration::from_millis(100)); for _ in 1 .. 5 { interval.tick().await; worker.common_task_step( SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 ).await; } } async fn run_worker(worker: Arc<dyn LongWorker>) { let mut tasks = worker.get_additional_tasks().await; tasks.push(common_task(worker.clone()).boxed_local()); futures::future::join_all(tasks).await; } #[tokio::main(worker_threads = 5)] async fn main() { let noop_worker = Arc::new(NoOpWorker {}); let sleep_worker = Arc::new(SleepWorker {}); run_worker(noop_worker).await; run_worker(sleep_worker).await; }
方法2:分离生命周期绑定
去掉trait的显式生命周期参数,使用匿名生命周期'_让编译器自动推断合理范围:
use std::time::SystemTime; use futures::{FutureExt, future::LocalBoxFuture}; use async_trait::async_trait; use tokio::time::{interval, sleep, Duration}; #[async_trait] trait LongWorker { async fn get_additional_tasks(&self) -> Vec<LocalBoxFuture<'_, ()>>; async fn common_task_step(&self, some_arg: u64); } struct NoOpWorker {} #[async_trait] impl LongWorker for NoOpWorker { async fn get_additional_tasks(&self) -> Vec<LocalBoxFuture<'_, ()>> { vec![] } async fn common_task_step(&self, some_arg: u64) { println!("Noop Got arg {}", some_arg); } } struct SleepWorker {} impl SleepWorker { async fn sleep(&self, millis: u64) { sleep(Duration::from_millis(millis)).await; println!("Slept for {}ms", millis); } } #[async_trait] impl LongWorker for SleepWorker { async fn get_additional_tasks(&self) -> Vec<LocalBoxFuture<'_, ()>> { vec![ self.sleep(500).boxed_local(), self.sleep(1500).boxed_local(), self.sleep(2500).boxed_local(), self.sleep(3500).boxed_local(), ] } async fn common_task_step(&self, some_arg: u64) { println!("Sleeper Got arg {}", some_arg); } } async fn common_task(worker: &dyn LongWorker) { let mut interval = interval(Duration::from_millis(100)); for _ in 1 .. 5 { interval.tick().await; worker.common_task_step( SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 ).await; } } async fn run_worker(worker: Box<dyn LongWorker>) { let mut tasks = worker.get_additional_tasks().await; tasks.push(common_task(&*worker).boxed_local()); futures::future::join_all(tasks).await; } #[tokio::main(worker_threads = 5)] async fn main() { let noop_worker = Box::new(NoOpWorker {}); let sleep_worker = Box::new(SleepWorker {}); run_worker(noop_worker).await; run_worker(sleep_worker).await; }
反模式说明
原实现将trait的生命周期参数同时绑定到self引用和返回Future,属于生命周期过度绑定的反模式。这种设计强制Future与self引用的生命周期完全对齐,但异步场景中Future的执行时机不可预测,编译器无法保证引用的有效性,必然触发生命周期错误。正确的做法是让Future持有Worker的所有权(如Arc),或使用匿名生命周期让编译器自动推断合理的生命周期范围。
内容的提问来源于stack exchange,提问作者balbok

