Async initializer in LazyLock

How can I initialize a function via async inside LazyLock?

here's my attempt so far:

pub static GLOBAL_VAR: LazyLock<tokio::sync::Mutex<GlobalVar<'static>>> =
    LazyLock::new(|| async { GlobalVar::new().await.unwrap() });

the new() method has this signature:

impl<'a> GlobalVar<'a> {
    pub async fn new<'b>() -> Result<Mutex<GlobalVar<'a>>> {..}
}

It works in lazy_static but I'm trying to refactor it to LazyLock instead (or whatever the best practice is).

Here's how I did it (successfully) with lazy_static:

   pub static ref GLOBAL_VAR: AsyncOnce<tokio::sync::Mutex<GlobalVar<'static>>> =
        AsyncOnce::new(async { GlobalVar::new().await.unwrap() });
1 Like

This isn't specific to LazyLock, the general problem is running async code in a sync context

Since you're using tokio, its method for that is runtime.block_on, which means you'll have to acquire a runtime first.

1 Like

I would ditch the lazy wrapper since you need synchronization with the mutex anyways.

static GLOBAL_VAR: Mutex<Option<GlobalVar>> = Mutex::const_new(None);

pub async fn get_global_var() -> Result<MappedMutexGuard<'static, GlobalVar>, InitError> {
    let mut opt_guard = GLOBAL_VAR.lock().await;
    if opt_guard.is_none() {
        *opt_guard = Some(GlobalVar::new().await?);
    }
    Ok(MutexGuard::map(opt_guard, |opt| opt.as_mut().unwrap()))
}

If this is called often you can split of the initialization and annotate it with #[cold].

2 Likes

Another option is to use OnceLock instead, have callers wait on it (currently on nightly) and set it on the main thread. While wait nominally blocks it won't actually block after initialization.

Or avoid global state if you can. E.g. actix has the app_data feature to handle state without making it global.

I tried this approach and I get this error:

thread 'main' panicked at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.38.0/src/runtime/scheduler/multi_thread/mod.rs:86:9:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

here's how the impl looked like:

pub static GLOBAL_VAR: LazyLock<tokio::sync::Mutex<GlobalVar<'static>>> =
    LazyLock::new(|| {
        Runtime::new()
            .unwrap()
            .block_on(GlobalVar::new())
            .unwrap()
    });

The actual initialization code runs when the future is polled, and that's not when the global is created, but when it's first accessed. You are probably accessing it for the first time once a runtime is already inplace.

Because access could need the tokio runtime, you will have to ensure the tokio runtime is set up and entered every time you use the global.

This whole setup seems silly. Does it have to be both lazy and global?

If it doesn't have to be lazy, you could create the value early in your program (eagerly in async fn main, not in any lazy callback), and synchronously assign it to the global.

3 Likes

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.