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.
| Tipo | Estável desde | Uso principal |
|---|---|---|
Once | Rust 1.0 | Executar código uma vez (sem armazenar valor) |
OnceLock<T> | Rust 1.70 | Inicializar e armazenar um valor uma vez |
LazyLock<T> | Rust 1.80 | Valor 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_staticpor 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
| Item | Descriçã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
| Item | Descriçã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
| Item | Descrição |
|---|---|
LazyLock::new(f) | Cria com closure de inicialização (executada na 1a vez) |
*lazy / Deref | Acessa 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>eLazyLock<T>sãoSync— podem ser usados emstaticcompartilhado entre threads.call_onceeget_or_initbloqueiam 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_onceentra em panic, oOncefica “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
- Mutex e RwLock em Rust — outra forma de sincronização thread-safe
- Tipos Atômicos — primitivas de baixo nível usadas internamente
- Send e Sync — por que essas primitivas são Sync
- Cell e RefCell — mutabilidade interior para thread única
- Padrões de Thread Safety — padrões avançados
- Documentação oficial:
std::sync::OnceLock