Running non-static CPU-bound computation in async function

I'm trying to run CPU-bound computations inside an async context. The recommendation I found is to run the calculation via a parallelism library (e.g., rayon) and send the result through a one-shot channel to the waiting async function. This leads to the code below.

#[derive(Debug)]
struct BigData(usize); // very expensive to clone

async fn foo<'a>(arg: &'a BigData) -> BigData { 
    let (sender, receiver) = tokio::sync::oneshot::channel();
    rayon::spawn(move || {
        let argument = arg; // <- ERROR: cannot captured `&'a BigData` here

        // Some long blocking computation here
        let result = BigData(argument.0);
        sender.send(result).unwrap();
    });
    receiver.await.unwrap()
}

The problem is that the computation references arg, which lasts only for 'a, but the spawn method (rightfully) requires the closure to be 'static. However, here, I know that the closure finishes its execution within 'a since foo won't return until sender.send is invoked (or the closure panic, which can probably be handled with catch_unwind).

Would it be sound to transmute the lifetime somewhere (e.g. extend arg lifetime)? Alternatively, are there other approaches to running a (sync) background task that captures stack variables of a code block, which requires the task completion to continue?

Your reasoning makes sense if you were in a synchronous function. However an async function produces a Future which could be dropped (or even leaked, which cannot be detected) after being polled. If that happens the lifetime 'a can end while the parallel task is still running, and that's unsound.

Edit: this is a pretty known issue without a good solution, see also The Scoped Task trilemma

1 Like

This is actually false. The future created by foo might be dropped anywhere an await happens. So it is quite possible for arg drops before computation complete. With that stated, transmuting lifetime is obviously unsound.

Unfortunately I don't see a very good way to resolve this issue robustly in an async context other than "Just use Arc".

1 Like

Thanks, my sync-brain forgot about future being dropped. Though the solution seem to be cloning/arc-ing the argument, which is a shame since I managed to get this far with proper lifetime.

Yeah, have to admit Rust has lot of rough edges on async stuff. If you are using a multithread tokio runtime, maybe tokio::task::block_in_place is enough? You need to be careful using it though (see doc for more info).

1 Like