OnceLock în Rust

Celulă care primește o valoare o singură dată, sigur între fire; ideală pentru valori globale calculate la prima folosire.

Sintaxă
use std::sync::OnceLock;

const fn new() -> OnceLock<T>
fn get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T

Ce face OnceLock

OnceLock<T> începe gol. Prima chemare a lui get_or_init(f) rulează funcția, păstrează rezultatul și returnează &T. Chemările următoare returnează direct valoarea deja salvată, fără să mai ruleze funcția. Dacă mai multe fire cheamă în același timp, doar unul rulează funcția, iar celelalte așteaptă.

Pentru că new() este const, un OnceLock poate fi static. Așa se fac valorile globale care nu pot fi calculate la compilare. Mai există get() (citește fără să inițializeze, dă Option) și set() (pune valoarea dacă celula e goală).

Parametri

Parametru Descriere
f Funcția care creează valoarea; rulează cel mult o dată.

Ce returnează

get_or_init: &T, referință la valoarea salvată.

Exemple

Exemplul 1: Configurare globală inițializată o dată

Mesajul „încarc configurarea” apare o singură dată, deși funcția config() este apelată de trei ori.

use std::sync::OnceLock;

struct Config {
    oras: String,
    tva: f64,
}

static CONFIG: OnceLock<Config> = OnceLock::new();

fn config() -> &'static Config {
    CONFIG.get_or_init(|| {
        println!("(încarc configurarea)");
        Config { oras: String::from("Iași"), tva: 0.19 }
    })
}

fn main() {
    println!("oraș: {}", config().oras);
    println!("TVA: {}", config().tva);
    println!("preț cu TVA: {:.2} lei", 100.0 * (1.0 + config().tva));
}

Rezultat

(încarc configurarea)
oraș: Iași
TVA: 0.19
preț cu TVA: 119.00 lei

Exemplul 2: Mai multe fire, o singură inițializare

use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

static APELURI: AtomicUsize = AtomicUsize::new(0);
static TABEL: OnceLock<Vec<u64>> = OnceLock::new();

fn patrate() -> &'static Vec<u64> {
    TABEL.get_or_init(|| {
        APELURI.fetch_add(1, Ordering::SeqCst);
        (0..10).map(|x| x * x).collect()
    })
}

fn main() {
    println!("înainte: {:?}", TABEL.get());
    thread::scope(|s| {
        for _ in 0..4 {
            s.spawn(|| patrate().len());
        }
    });
    println!("pătrate: {:?}", patrate());
    println!("inițializări: {}", APELURI.load(Ordering::SeqCst));
}

Rezultat

înainte: None
pătrate: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
inițializări: 1

Greșeli frecvente

  • Folosești static mut pentru o valoare globală: accesul este unsafe și, din ediția 2024, referințele la static mut sunt eroare. Folosește OnceLock sau LazyLock.
  • Apelezi din funcția de inițializare tot get_or_init pe aceeași celulă: firul se blochează (sau intră în panică).

Comenzi înrudite

  • LazyLock: Valoare calculată automat la prima folosire, sigur între fire; se folosește ca static global.
  • Mutex: Protejează o valoare folosită din mai multe fire: lock() așteaptă până când firul curent este singurul cu acces.
  • Option::get_or_insert_with: Dacă opțiunea este None, pune în ea valoarea calculată de funcție; apoi returnează o referință mutabilă la valoare.
  • Box::leak: Renunță la un Box fără să elibereze memoria și dă o referință valabilă până la finalul programului (&’static).