Lifetime issue with wrapping non-async in async

Is it possible to make the following work?

    pub async fn hash_passphrase_async<'a, 'b>(
        &'a self,
        pass_phrase: &'b [u8],
    ) -> Result<String, Error> {
        let blocking_task = tokio::task::spawn_blocking(|| self.hash_passphrase(pass_phrase));
        blocking_task.await?
    }

When I try to make this work it says that both 'a and 'b escape out of the method body. Is there something I can do with the blocking task to make this work or is this just not possible.

I don't have a direct answer other than to pass owned values rather than references to spawn_blocking. A Vec<u8> could be passed and returned, if you want to avoid any added allocations.

But if you're wondering whether you really need to use spawn_blocking, Alice Rhyl's article What is Blocking? mentions a rule of thumb:

To give a sense of scale of how much time is too much, a good rule of thumb is no more than 10 to 100 microseconds between each .await. That said, this depends on the kind of application you are writing.

I think it wouldn’t even be sound, unfortunately. Spawning a task like this means the code can keep running in the background even if the future for the hash_passphrase_async invocation is cancelled; which could mean use-after-free.

It’s a known shortcoming of async that structured concurrency, such as thread::scope-style APIs, can’t really work.[1]


  1. at least for the time being, without new solutions e.g. from language design for non-cancellable futures ↩︎

1 Like

That's what I figured. I couldn't see a way to make the lifetimes work, but I was hoping I was missing something and that there was a way to make it work.

To be clear, structured concurrency can work with async, but you won't be able to soundly translate it to the lifetime guarantees needed for APIs similar to thread::scope.

The usual way I deal with this is to use Arc<Self> instead of &self. It's a bit ugly, but gets the job done.

2 Likes

In addition to the answers mentioned above, you can use unsafe blocks to pass a pointer directly to your thread but you have to guarantee safety yourself, or use Box::leak to turn your data into a 'static that lives for the whole duration of your program, which is a safer option with the tradeoff of leakage,
UPDATE: the unsafe option is obviously not recommended there are ton of ways that can go wrong without you realizing

Tokio has last-resort hack of block_in_place that runs the blocking code on the same thread, so it's compatible with temporary loans. The polling thread will have to stay blocked, so it may still negatively affect performance of the tokio runtime.

1 Like