Context
Recently I found myself writing an async function that performs an expensive computation. Something in the lines of:
async fn my_async_fn(buf: &mut Vec<u8>) {
// ...
// (async operations omitted for brevity)
// Problem: the function below will block the
// thread because it takes long to complete!
expensive_computation(buf)
}
fn expensive_computation(_buf: &mut Vec<u8>) {
// Body omitted for brevity
}
As far as I know, there is a problem with this implementation because it can block the futures on the current thread from progressing.
Current solution
The solution to the problem above is to offload the expensive computation to a different thread, using a method such as tokio::task::spawn_blocking
. However, spawn_blocking
has the 'static
bound on the passed closure, making it impossible to do the straightforward thing:
async fn my_async_fn(buf: &mut Vec<u8>) {
// Compile error: `&mut buf` does not satisfy the 'static lifetime
tokio::task::spawn_blocking(move || expensive_computation(buf))
.await
.unwrap()
}
A somewhat straightforward solution to the compile error is to change the buf
parameter to be of type Arc<Mutex<Vec<u8>>
. In my view that is undesirable, because the fact that we are using spawn_blocking
here is an implementation detail of my_async_fn
and should not leak to its parameter types (i.e. the caller shouldn't need to be aware of it). Furthermore, using Arc
and Mutex
feels like overkill, because I know buf
will outlive the spawned task (I am using await
right after the spawn_blocking
call).
Because I was unsatisfied, I kept fiddling with the code until I settled on the good old mem::swap
trick, and got the following to compile, without having to change my_async_fn
's signature:
async fn my_async_fn(buf: &mut Vec<u8>) {
let mut owned_buf = Vec::default();
std::mem::swap(buf, &mut owned_buf);
*buf = tokio::task::spawn_blocking(move || {
expensive_computation(&mut owned_buf);
owned_buf
})
.await
.unwrap()
}
Basically: I ensure the spawned task operates on an owned Vec<u8>
, thereby getting rid of lifetime issues, and pass ownership back to buf
once the spawned task is done.
My question
The final approach using mem::swap
feels like a perfect solution in this case, but there is one big drawback that might prevent it from working in other situations: it requires a sensible default value you can use with mem::swap
. That means, for instance, that the approach would not work if expensive_computation
operated on a &mut File
, because there is no such thing as File::default()
.
Is this just the way it is, making it impossible to use spawn_blocking
without modifying the function's signature? Or is there some other solution/workaround I'm missing?
By the way, I have created a playground link in case you want to try things out!