Hi! I'm trying to write a simple async queue, but went into trouble when dealing with inputs with reference:
I want to spawn a task which takes a reference as inputs (let's call its lifetime 'a).
I'm sure that the lifetime of the async queue ('b) is shorter than 'a (which means, 'a: 'b).
However any tokio::spawn ish operations requires reference to be 'static.
So I have to transmute the inputs because I'm sure the inputs ('a) will live long enough until the queue ('b) is completely clean up.
I'm struggling with: whether I can avoid the transmute here? Or am I missing something, that even 'a: 'b, the above sequences will lead to UB so I cannot do it safely?
I said UB, not bugs, because I'm already using transmute and everything seems to be working smoothly...
Here's the (pseudo) codes I've abstracted out, feel free to skip it if you already know what my problem is:
impl<T, R> AsyncQueue<T, R>
where
T: Send + 'static,
R: Send + 'static,
{
// what should I do if `T` here contains references? (e.g., &'a [f32])
pub fn submit(&mut self, id: usize, data: T) {
let worker = Arc::clone(&self.worker);
let results = Arc::clone(&self.results);
self.rt.spawn(async move {
let result = worker.read().unwrap().process(data);
results.lock().unwrap().insert(id, result);
});
}
pub fn pop(&self, id: usize) -> Option<Result<R>> {
self.results.lock().unwrap().remove(&id)
}
}
It happens to be the case that the destructor of Runtime does approximately the same as reset.
Anyway, it's possible that your code uses AsyncQueue in a way that does not trigger UB, but generally good Rust API design says that users of your API must not be able to trigger UB in any way using only safe methods. The problem with your transmute is that the user can mem::forget your AsyncQueue. You can read about it in the nomicon or this old (more informal) article.
You can do what you're trying to do safely via scope_and_block from the async-scoped crate. Basically, your struct should look like this:
This way, it becomes possible to spawn futures as long as they have no lifetime annotations shorter than 'a, where 'a will be the duration of a call to scope_and_block somewhere in your program.
Note that this strategy works only because your code runs from outside of the runtime context. This would not work from an async fn because scope_and_block doesn't work there. (It blocks the thread). I normally do not recommend use of async-scoped, and you're in one of the very rare cases where it can be used.
As written, all of your Workers have a non-async process() function. A more traditional thread pool or something like rayon might be a better fit than tokio for this application.
You can spawn a background task with rayon using rayon::spawn. That said, you probably want rayon::scope for similar reasons as why tokio::spawn was causing you issues.
Maybe I'm too newbie, but from what I've known right now, I don't have something like Runtime in rayon, so I cannot spawn it to the background with scope bounded, no?
Or maybe you are suggesting spawning a rayon::scope + rayon::spawn inside the rt.spawn?
You call rayon::scope to create a scope that tasks cannot escape. That gives you a Scope object and you use the Scope::spawn method to spawn tasks bounded by the scope.
Rayon is used instead of Tokio here, not together with Tokio.
Darn, I've always considered rayon::scope simply as a magic function that helps me manage moves and lifetimes.
So it seems to be using the Scope struct behind the scene, I'll look into it and study. Thank you again for pointing out the resources and for the detailed explanations!!!