Async equivalent of `once_cell::Lazy`

I'm looking for a way to spawn a computation in the background and then await the result multiple times and get a reference to the computed result, similar to once_cell::Lazy except that it wouldn't provide the value on deref() but on on await.

It's not a precise equivalent, but see this example from the documentation for tokio::sync::OnceCell.

use tokio::sync::OnceCell;

static ONCE: OnceCell<u32> = OnceCell::const_new();

async fn get_global_integer() -> &'static u32 {
    ONCE.get_or_init(|| async {
        1 + 1
    }).await
}

#[tokio::main]
async fn main() {
    let result = get_global_integer().await;
    assert_eq!(*result, 2);
}
1 Like

Actually, it seems like I misread your question. If your value is not a global, then you probably want to wrap the JoinHandle from spawning your task in a Shared instead.

2 Likes

shared is the right direction, thanks. What I settled on is roughly

async fn foo() {
  let task = async {
     let result = // some queries
     Arc::new(result)
  }.shared();

  {
    let task = task.clone()
    // try running it in the background
    tokio::spawn(async {
        task.await;      
    });
  }

  for bar in bars {
     let another_result = bar.quux().await;
     let precomputed = task.await;
     // ...
  }
}

I would recommend wrapping the JoinHandle rather than the underlying future. Otherwise, precomputed = task.await might end up being the line that actually runs the future.

I don't understand the difference. If the result isn't ready by then it would have to wait anyway. And spawn says it'll start running immediately.

Yes, it does start running immediately, but there's a timing question here. You could reach the precomputed = task.await line before you reach task.await inside tokio::spawn.

I see. I think this won't be an issue in my case since there are some other things to await before I use the shared inside the loop.

My intent is to run a few things concurrently but I can't use join! since one of the values is a batch query that only runs once while the other parts (the loop) are individual queries where each needs to be combined with a part of the patch. In expectation the loop only runs one iteration but rarely it might be a few more.

In case it wasn't clear, this is what I am suggesting:

async fn foo() {
  let task = tokio::spawn(async {
     let result = // some queries
     Arc::new(result)
  }).shared();

  for bar in bars {
     let another_result = bar.quux().await;
     let precomputed = task.await;
     // ...
  }
}

Not anything with join!.

No confusion there, I was just trying to explain the execution graph.

Awaiting a JoinHandle would introduce an extra Result indirection and the error type isn't Clone which is required by Shared.

This should work:

async fn foo() {
  let task = tokio::spawn(async {
     let result = // some queries
     Arc::new(result)
  });
  
  let task = async move { task.await.unwrap() }.shared();

  for bar in bars {
     let another_result = bar.quux().await;
     let precomputed = task.await;
     // ...
  }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.