Once, OnceLock e LazyLock em Rust

Guia completo de Once, OnceLock e LazyLock em Rust: inicialização global thread-safe, lazy statics e substituição do lazy_static em português.

O que faz e quando usar

Rust oferece três primitivas na biblioteca padrão para inicialização única e thread-safe: Once, OnceLock<T> e LazyLock<T>. Todas garantem que um bloco de código seja executado exatamente uma vez, mesmo quando múltiplas threads tentam simultaneamente.

TipoEstável desdeUso principal
OnceRust 1.0Executar código uma vez (sem armazenar valor)
OnceLock<T>Rust 1.70Inicializar e armazenar um valor uma vez
LazyLock<T>Rust 1.80Valor lazy com closure de inicialização

Use essas primitivas quando:

  • Precisa de um singleton ou variável global inicializada uma vez.
  • Quer substituir a crate lazy_static por funcionalidade da biblioteca padrão.
  • Precisa de configuração global lida de arquivo ou variável de ambiente.
  • Quer inicializar pools de conexão, caches ou loggers na primeira chamada.

Tipos e Funções Principais

Once

ItemDescrição
Once::new()Cria uma nova instância Once
once.call_once(f)Executa f exatamente uma vez; bloqueia threads concorrentes
once.is_completed()Retorna true se call_once já foi executado com sucesso

OnceLock

ItemDescrição
OnceLock::new()Cria um OnceLock vazio
lock.get()Retorna Option<&T>None se ainda não foi inicializado
lock.get_or_init(f)Retorna &T, inicializando com f se necessário
lock.set(value)Define o valor; retorna Err(value) se já foi definido
lock.get_mut()Retorna Option<&mut T> (requer &mut self)

LazyLock

ItemDescrição
LazyLock::new(f)Cria com closure de inicialização (executada na 1a vez)
*lazy / DerefAcessa o valor (inicializa na primeira vez)
LazyLock::force(&lazy)Força a inicialização sem desreferenciar

Exemplos de Código

Once — executar código uma vez

use std::sync::Once;

static INIT: Once = Once::new();

fn inicializar() {
    INIT.call_once(|| {
        println!("Inicialização executada!");
        // Configurar logger, abrir conexão, etc.
    });
}

fn main() {
    // Todas chamam, mas só a primeira executa a closure
    inicializar(); // imprime "Inicialização executada!"
    inicializar(); // não imprime nada
    inicializar(); // não imprime nada

    println!("Já foi inicializado? {}", INIT.is_completed());
}

Once com múltiplas threads

use std::sync::Once;
use std::thread;

static INIT: Once = Once::new();

fn main() {
    let mut handles = vec![];

    for id in 0..5 {
        handles.push(thread::spawn(move || {
            println!("Thread {} tentando inicializar...", id);
            INIT.call_once(|| {
                println!("==> Thread {} executou a inicialização!", id);
                // Simular trabalho de inicialização
                thread::sleep(std::time::Duration::from_millis(100));
            });
            println!("Thread {} continuando após inicialização.", id);
        }));
    }

    for h in handles {
        h.join().unwrap();
    }
    // Apenas UMA thread terá executado a closure
}

OnceLock — armazenar valor inicializado

use std::sync::OnceLock;

// Variável global que será inicializada uma vez
static CONFIG: OnceLock<Config> = OnceLock::new();

struct Config {
    db_url: String,
    max_conexoes: u32,
    modo_debug: bool,
}

fn obter_config() -> &'static Config {
    CONFIG.get_or_init(|| {
        println!("Carregando configuração...");
        Config {
            db_url: std::env::var("DATABASE_URL")
                .unwrap_or_else(|_| "postgres://localhost/mydb".into()),
            max_conexoes: 10,
            modo_debug: cfg!(debug_assertions),
        }
    })
}

fn main() {
    // Primeira chamada inicializa
    let cfg = obter_config();
    println!("DB: {}", cfg.db_url);

    // Chamadas subsequentes retornam o valor já inicializado
    let cfg2 = obter_config();
    println!("Max conexões: {}", cfg2.max_conexoes);
}

OnceLock com set() explícito

use std::sync::OnceLock;

static LOGGER: OnceLock<String> = OnceLock::new();

fn configurar_logger(nivel: &str) -> Result<(), String> {
    LOGGER.set(nivel.to_string()).map_err(|val| {
        format!("Logger já configurado com: {}", val)
    })
}

fn log(msg: &str) {
    if let Some(nivel) = LOGGER.get() {
        println!("[{}] {}", nivel, msg);
    } else {
        println!("[sem logger] {}", msg);
    }
}

fn main() {
    log("antes da config");  // [sem logger] antes da config

    configurar_logger("INFO").unwrap();
    log("após primeira config"); // [INFO] após primeira config

    // Tentar configurar novamente falha
    match configurar_logger("DEBUG") {
        Ok(()) => println!("Reconfigurado"),
        Err(e) => println!("Erro: {}", e), // Erro: Logger já configurado com: INFO
    }

    log("continua INFO"); // [INFO] continua INFO
}

LazyLock — substituto do lazy_static!

LazyLock combina a declaração e a closure de inicialização em um único tipo:

use std::sync::LazyLock;
use std::collections::HashMap;

// Substituição direta do lazy_static!
static MAPA_CORES: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
    println!("Inicializando mapa de cores...");
    let mut m = HashMap::new();
    m.insert("vermelho", "#FF0000");
    m.insert("verde", "#00FF00");
    m.insert("azul", "#0000FF");
    m.insert("amarelo", "#FFFF00");
    m
});

static REGEX_EMAIL: LazyLock<regex::Regex> = LazyLock::new(|| {
    // Compila a regex apenas uma vez
    regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});

fn main() {
    // Primeira vez: inicializa o mapa
    println!("Vermelho: {}", MAPA_CORES["vermelho"]);

    // Segunda vez: já inicializado, retorno imediato
    println!("Azul: {}", MAPA_CORES["azul"]);

    // Exemplo com regex (requer crate regex no Cargo.toml)
    // println!("Email válido: {}", REGEX_EMAIL.is_match("user@example.com"));
}

Comparação: antes e depois do LazyLock

// ANTES (com crate lazy_static):
//
// use lazy_static::lazy_static;
// lazy_static! {
//     static ref DADOS: Vec<i32> = {
//         println!("Inicializando...");
//         vec![1, 2, 3, 4, 5]
//     };
// }

// DEPOIS (biblioteca padrão, sem dependência externa):
use std::sync::LazyLock;

static DADOS: LazyLock<Vec<i32>> = LazyLock::new(|| {
    println!("Inicializando...");
    vec![1, 2, 3, 4, 5]
});

fn main() {
    println!("Antes de acessar DADOS");
    println!("Soma: {}", DADOS.iter().sum::<i32>());
    println!("Soma de novo: {}", DADOS.iter().sum::<i32>());
}

OnceLock em struct (não-global)

OnceLock também é útil para inicialização lazy dentro de structs:

use std::sync::OnceLock;

struct Recurso {
    nome: String,
    // Inicializado sob demanda
    cache: OnceLock<Vec<String>>,
}

impl Recurso {
    fn new(nome: &str) -> Self {
        Recurso {
            nome: nome.to_string(),
            cache: OnceLock::new(),
        }
    }

    fn obter_dados(&self) -> &[String] {
        self.cache.get_or_init(|| {
            println!("Carregando dados de '{}'...", self.nome);
            // Simular busca demorada
            vec![
                format!("{}-item1", self.nome),
                format!("{}-item2", self.nome),
                format!("{}-item3", self.nome),
            ]
        })
    }
}

fn main() {
    let recurso = Recurso::new("banco");

    // Primeira chamada: carrega dados
    println!("Dados: {:?}", recurso.obter_dados());

    // Segunda chamada: retorna do cache
    println!("Dados (cache): {:?}", recurso.obter_dados());
}

Padrões Comuns e Anti-padrões

Padrão: pool de conexões global

use std::sync::OnceLock;

// Simular um pool de conexões
struct DbPool {
    url: String,
    max_conn: u32,
}

impl DbPool {
    fn new(url: &str, max: u32) -> Self {
        println!("Criando pool para: {}", url);
        DbPool {
            url: url.to_string(),
            max_conn: max,
        }
    }

    fn query(&self, sql: &str) -> String {
        format!("[{}] Executando: {}", self.url, sql)
    }
}

static POOL: OnceLock<DbPool> = OnceLock::new();

fn db() -> &'static DbPool {
    POOL.get_or_init(|| {
        DbPool::new("postgres://localhost/app", 10)
    })
}

fn main() {
    println!("{}", db().query("SELECT 1"));
    println!("{}", db().query("SELECT * FROM users"));
    // Pool criado apenas uma vez
}

Anti-padrão: panic na closure de Once

use std::sync::Once;

static INIT: Once = Once::new();

fn main() {
    // Se a closure de call_once entra em panic, o Once fica "envenenado"
    let resultado = std::panic::catch_unwind(|| {
        INIT.call_once(|| {
            panic!("Erro na inicialização!");
        });
    });
    println!("Primeiro call_once: {:?}", resultado); // Err(...)

    // Chamadas subsequentes TAMBÉM vão entrar em panic!
    let resultado2 = std::panic::catch_unwind(|| {
        INIT.call_once(|| {
            println!("Esta closure nunca executa");
        });
    });
    println!("Segundo call_once: {:?}", resultado2); // Err(PoisonError)

    // Para evitar: use get_or_init com tratamento de erro
    // ou garanta que a closure nunca entra em panic
}

Garantias de Thread Safety

  • Once, OnceLock<T> e LazyLock<T> são Sync — podem ser usados em static compartilhado entre threads.
  • call_once e get_or_init bloqueiam threads concorrentes até a inicialização completar.
  • A closure é executada exatamente uma vez, mesmo com centenas de threads chamando simultaneamente.
  • Se a closure de Once::call_once entra em panic, o Once fica “envenenado” e chamadas futuras também entram em panic.
  • OnceLock::get_or_init é seguro contra poison — se a closure falhar, outra thread pode tentar novamente.

Veja Também