Tokio spawn & lifetime
Excerpt from the Tokio documentation
When you spawn a task on the Tokio runtime, its type's lifetime must be 'static. This means that the spawned task must not contain any references to data owned outside the task.
Run this to show the compiler error when this requirement is no met.
use tokio::task::JoinSet; use tokio::time::{sleep, Duration}; async fn compute(item: &str) { println!("{}", item); } #[tokio::main] async fn main() { let v: Vec<String> = vec![String::from("john"), String::from("doe")]; let mut v_ref = Vec::<&str>::with_capacity(v.len()); for item in &v { v_ref.push(item); } let mut join_set = JoinSet::new(); for item in v_ref { join_set.spawn(async { compute(item).await }); } sleep(Duration::from_millis(100)).await; }
The fact that the drop of v after main is not compatible with "argument requires that `v` is borrowed for `'static`"
is not obvious.
The issue is that v has the lifetime defined by the scope of main() and not the 'static lifetime required by the Trait bound1 of the spawn function.
The solution for Tokio API is Arc
.
use std::sync::Arc; use tokio::task::JoinSet; use tokio::time::{Duration, sleep}; async fn compute(item: Arc<str>) { println!("{}", item) } #[tokio::main] async fn main() { let v: Vec<String> = vec![String::from("john"), String::from("doe")]; let mut v_ref = Vec::<Arc<str>>::with_capacity(v.len()); for item in &v { v_ref.push(Arc::from(item.as_str())); } let mut join_set = JoinSet::new(); for item in v_ref { join_set.spawn(async { compute(item).await }); } sleep(Duration::from_millis(100)).await; }
We can minimize the same issue without Tokio.
use std::fmt::Debug; fn compute(item: impl Debug + 'static) { println!("{:?}", item); } fn main() { let v: Vec<String> = vec![String::from("john"), String::from("doe")]; let mut v_ref = Vec::<&str>::with_capacity(v.len()); for item in &v { v_ref.push(item); } for item in v_ref { compute(item); } }
And solved it the same way with Arc
.
use std::{fmt::Debug, sync::Arc}; fn compute(item: impl Debug + 'static) { println!("{:?}", item); } fn main() { let v: Vec<String> = vec![String::from("john"), String::from("doe")]; let mut v_ref = Vec::<Arc<str>>::with_capacity(v.len()); for item in &v { v_ref.push(Arc::from(item.as_str())); } for item in v_ref { compute(item); } }
But why is the Tokio requirement so strong ?
If the requirement was simply a regular lifetime we can do :
fn compute<'a>(item: impl std::fmt::Debug + 'a) { println!("{:?}", item) } fn main() { let v: Vec<String> = vec![String::from("john"), String::from("doe")]; let mut v_ref = Vec::<&str>::with_capacity(v.len()); for item in &v { v_ref.push(item); } for item in v_ref { compute(item); } }
It turn out2 that the async executor has exactly this requirement.
spawn in smol is defined ike this :
fn spawn<T: Send + 'a>(&self, future: impl Future<Output = T> + Send + 'a) -> Task<T>
whereas Tokio spawn is defined ike this :
pub fn spawn<F>(&mut self, task: F) -> AbortHandle
where
F: Future<Output = T>,
F: Send + 'static,
T: Send,
So I would expect this code to compile :
use async_executor::Executor;
async fn compute<'a>(item: impl std::fmt::Debug + 'a) {
println!("{:?}", item)
}
fn main() {
let ex: Executor = Executor::new();
let v: Vec<String> = vec![String::from("john"), String::from("doe")];
let mut v_ref: Vec<&str> = Vec::<&str>::with_capacity(v.len());
for item in &v {
v_ref.push(item);
}
for item in v_ref {
let _task = ex.spawn(async {
compute(item).await;
});
}
}