rust

Calling function only once with return type


A function is getting called from two completely different places and I want to run that function only once in a threaded safe environment. Consider this scenario:

use std::sync::{Once, Mutex};
use lazy_static::lazy_static;

lazy_static! {
    static ref INIT: Once = Once::new();
    static ref VALUE: Mutex<Option<bool>> = Mutex::new(None);
}

fn expensive_function() -> bool {
    println!("Expensive computation happening...");
    false
}

fn get_value() -> bool {
    INIT.call_once(|| {
        *VALUE.lock().unwrap() = Some(expensive_function());
    });
    VALUE.lock().unwrap().unwrap()
}

fn main() {
    // First call - will run expensive_function
    println!("First call: {}", get_value());
    
    // Second call - will return cached value
    println!("Second call: {}", get_value());
}

This works and gives the ouput as expected and is using the mutexes to take care of threaded environment.

Expensive computation happening... First call: false Second call: false

However, there are certain cases which I am not able to think and need help from the experts. 1. What if the thread is poisned and 2. So many unwraps() are not good I believe.

What are the suggestions from the community here? The second call could be in other files as well.

I have tried making taking care of poisened thread explicitly and I could not make it work. Also tried with match statements to avoid unwraps and got confused.


Solution

  • As mentioned in the comments, using std::sync::LazyLock allows you to get rid of both Option and Mutex, eliminating both unwraps:

    fn get_value() -> bool {
        static VALUE: LazyLock<bool> = LazyLock::new(|| expensive_function());
        *VALUE
    }
    

    Playground

    If you need to pass arguments to expensive_function(), use OnceLock instead:

    fn get_value() -> bool {
        static VALUE: OnceLock<bool> = OnceLock::new();
        // here you can pass arbitrary data to expensive_function()
        *VALUE.get_or_init(|| expensive_function())
    }
    

    Playground

    If you actually do need to occasionally modify the data, then you'll still need the Mutex and its unwrap due to poisoning. This unwrap is something you learn to accept as a pattern - after all, if the mutex is really poisoned (the thread that held it panicked), then you probably want to propagate the panic to the threads that attempt to use it. And if you still find the mutex-related unwrap() unpalatable, you can switch from std::sync::Mutex to parking_lot::Mutex, which doesn't have the poisoning feature.