O que faz e quando usar
O módulo std::sync::atomic fornece tipos que permitem operações atômicas — leituras e escritas que são indivisíveis do ponto de vista de outras threads. Diferentemente de um Mutex, operações atômicas não usam locks do sistema operacional; elas são implementadas diretamente em instruções do processador, tornando-as extremamente rápidas.
Use tipos atômicos quando:
- Você precisa compartilhar valores simples (bool, inteiros, ponteiros) entre threads.
- Quer um contador ou flag acessível por múltiplas threads sem o overhead de um Mutex.
- Está implementando estruturas de dados lock-free.
- Precisa de spin locks, contadores de referência ou flags de cancelamento.
Não use atômicos quando:
- Precisa proteger dados complexos (structs, vetores, strings) — use
MutexouRwLock. - Não entende as implicações de memory ordering — use
Mutexque é mais simples e seguro.
Tipos e Funções Principais
| Tipo | Descrição |
|---|---|
AtomicBool | Booleano atômico |
AtomicI8/I16/I32/I64 | Inteiros com sinal atômicos |
AtomicU8/U16/U32/U64 | Inteiros sem sinal atômicos |
AtomicUsize / AtomicIsize | Inteiros do tamanho de ponteiro atômicos |
AtomicPtr<T> | Ponteiro atômico |
Operações comuns
| Método | Descrição |
|---|---|
load(ordering) | Lê o valor atomicamente |
store(val, ordering) | Escreve o valor atomicamente |
swap(val, ordering) | Troca o valor e retorna o anterior |
compare_exchange(cur, new, ok, fail) | CAS: troca se o valor atual é cur |
fetch_add(val, ordering) | Soma e retorna o valor anterior |
fetch_sub(val, ordering) | Subtrai e retorna o valor anterior |
fetch_or(val, ordering) | OR bit-a-bit e retorna o anterior |
fetch_and(val, ordering) | AND bit-a-bit e retorna o anterior |
Memory Ordering — Ordenação de Memória
Este é o conceito mais importante (e mais confuso) dos atômicos. O Ordering define quais garantias de visibilidade a operação oferece em relação a outras operações de memória.
Mais restritivo (mais lento, mais garantias)
┌─────────────────────────────────────────┐
│ SeqCst — Ordem total entre threads │
│ AcqRel — Acquire + Release juntos │
│ Release — Publica dados para leitores │
│ Acquire — Consome dados publicados │
│ Relaxed — Sem garantias de ordem │
└─────────────────────────────────────────┘
Menos restritivo (mais rápido, menos garantias)
| Ordering | Quando usar |
|---|---|
Relaxed | Contadores simples onde a ordem não importa |
Acquire | Leitura que precisa ver os dados escritos antes de um Release |
Release | Escrita que publica dados para quem fizer Acquire |
AcqRel | Operações de leitura+escrita (como compare_exchange) |
SeqCst | Quando precisa de ordem total consistente entre todas as threads |
Regra de ouro: se você não tem certeza, use SeqCst. É o mais lento, mas o mais seguro. Otimize depois se for necessário.
Padrão Acquire/Release
Thread A (escrita) Thread B (leitura)
───────────────── ─────────────────
dados = 42;
flag.store(true, Release); --> flag.load(Acquire) == true
// Agora dados == 42 é garantido!
A escrita com Release garante que todas as escritas anteriores são visíveis para quem ler com Acquire.
Exemplos de Código
AtomicBool — flag de cancelamento
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn main() {
let cancelado = Arc::new(AtomicBool::new(false));
// Worker thread
let cancelado_clone = Arc::clone(&cancelado);
let worker = thread::spawn(move || {
let mut iteracoes = 0u64;
while !cancelado_clone.load(Ordering::Relaxed) {
// Simular trabalho
iteracoes += 1;
if iteracoes % 1_000_000 == 0 {
println!("Worker: {} iterações...", iteracoes);
}
}
println!("Worker cancelado após {} iterações", iteracoes);
iteracoes
});
// Deixar o worker rodar por 100ms
thread::sleep(Duration::from_millis(100));
cancelado.store(true, Ordering::Relaxed);
println!("Sinal de cancelamento enviado!");
let total = worker.join().unwrap();
println!("Total de iterações: {}", total);
}
AtomicUsize — contador compartilhado
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let contador = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contador = Arc::clone(&contador);
handles.push(thread::spawn(move || {
for _ in 0..1_000 {
// fetch_add retorna o valor ANTERIOR à soma
contador.fetch_add(1, Ordering::Relaxed);
}
}));
}
for h in handles {
h.join().unwrap();
}
// Sempre será 10.000 — operações atômicas são indivisíveis
println!("Contador: {}", contador.load(Ordering::Relaxed));
}
compare_exchange — CAS (Compare-And-Swap)
A operação CAS é a base de muitos algoritmos lock-free. Ela troca o valor apenas se o valor atual for o esperado:
use std::sync::atomic::{AtomicI32, Ordering};
fn main() {
let valor = AtomicI32::new(5);
// Tentar trocar: se for 5, mude para 10
match valor.compare_exchange(5, 10, Ordering::SeqCst, Ordering::SeqCst) {
Ok(anterior) => println!("Trocou! Valor anterior: {}", anterior),
Err(atual) => println!("Não trocou. Valor atual: {}", atual),
}
// Tentar trocar: se for 5, mude para 20 (vai falhar, pois agora é 10)
match valor.compare_exchange(5, 20, Ordering::SeqCst, Ordering::SeqCst) {
Ok(anterior) => println!("Trocou! Anterior: {}", anterior),
Err(atual) => println!("Não trocou. Atual: {}", atual),
}
println!("Valor final: {}", valor.load(Ordering::SeqCst));
}
Spin lock simples com atômicos
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
struct SpinLock {
locked: AtomicBool,
}
impl SpinLock {
fn new() -> Self {
SpinLock {
locked: AtomicBool::new(false),
}
}
fn lock(&self) {
// Tentar trocar false -> true repetidamente
while self
.locked
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
// Spin: aguardar até o lock ser liberado
// hint::spin_loop() otimiza o consumo de CPU
std::hint::spin_loop();
}
}
fn unlock(&self) {
self.locked.store(false, Ordering::Release);
}
}
fn main() {
let lock = Arc::new(SpinLock::new());
let contador = Arc::new(std::sync::atomic::AtomicI32::new(0));
let mut handles = vec![];
for _ in 0..4 {
let lock = Arc::clone(&lock);
let contador = Arc::clone(&contador);
handles.push(thread::spawn(move || {
for _ in 0..1_000 {
lock.lock();
// Seção crítica
let val = contador.load(Ordering::Relaxed);
contador.store(val + 1, Ordering::Relaxed);
lock.unlock();
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("Contador: {}", contador.load(Ordering::SeqCst));
// Sempre 4000
}
Acquire/Release — publicando dados
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let dados = Arc::new(std::sync::Mutex::new(0));
let pronto = Arc::new(AtomicBool::new(false));
let dados_clone = Arc::clone(&dados);
let pronto_clone = Arc::clone(&pronto);
// Produtor
let produtor = thread::spawn(move || {
// Preparar os dados
*dados_clone.lock().unwrap() = 42;
// Publicar com Release: garante que a escrita acima é visível
pronto_clone.store(true, Ordering::Release);
});
// Consumidor
let dados_clone = Arc::clone(&dados);
let pronto_clone = Arc::clone(&pronto);
let consumidor = thread::spawn(move || {
// Esperar com Acquire: garante ver as escritas antes do Release
while !pronto_clone.load(Ordering::Acquire) {
std::hint::spin_loop();
}
// Agora é garantido que dados == 42
let valor = *dados_clone.lock().unwrap();
println!("Consumidor leu: {}", valor);
assert_eq!(valor, 42);
});
produtor.join().unwrap();
consumidor.join().unwrap();
}
Gerador de IDs único
use std::sync::atomic::{AtomicU64, Ordering};
static PROXIMO_ID: AtomicU64 = AtomicU64::new(1);
fn gerar_id() -> u64 {
PROXIMO_ID.fetch_add(1, Ordering::Relaxed)
}
fn main() {
let mut handles = vec![];
for _ in 0..4 {
handles.push(std::thread::spawn(|| {
let mut ids = Vec::new();
for _ in 0..5 {
ids.push(gerar_id());
}
ids
}));
}
let mut todos_ids: Vec<u64> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
todos_ids.sort();
println!("IDs gerados: {:?}", todos_ids);
// Todos serão únicos: [1, 2, 3, ..., 20]
assert_eq!(todos_ids.len(), 20);
}
Padrões Comuns e Anti-padrões
Padrão: estatísticas atômicas
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
struct Metricas {
requisicoes: AtomicU64,
erros: AtomicU64,
bytes_processados: AtomicU64,
}
impl Metricas {
fn new() -> Self {
Metricas {
requisicoes: AtomicU64::new(0),
erros: AtomicU64::new(0),
bytes_processados: AtomicU64::new(0),
}
}
fn registrar_requisicao(&self, bytes: u64) {
self.requisicoes.fetch_add(1, Ordering::Relaxed);
self.bytes_processados.fetch_add(bytes, Ordering::Relaxed);
}
fn registrar_erro(&self) {
self.erros.fetch_add(1, Ordering::Relaxed);
}
fn relatorio(&self) -> String {
format!(
"Requisições: {}, Erros: {}, Bytes: {}",
self.requisicoes.load(Ordering::Relaxed),
self.erros.load(Ordering::Relaxed),
self.bytes_processados.load(Ordering::Relaxed),
)
}
}
fn main() {
let metricas = Arc::new(Metricas::new());
let mut handles = vec![];
for _ in 0..4 {
let metricas = Arc::clone(&metricas);
handles.push(thread::spawn(move || {
for i in 0..100 {
metricas.registrar_requisicao(1024);
if i % 10 == 0 {
metricas.registrar_erro();
}
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", metricas.relatorio());
}
Anti-padrão: Ordering errado
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
// ERRADO: usar Relaxed quando precisa de sincronização de dados
fn exemplo_errado() {
let dados = AtomicI32::new(0);
let pronto = AtomicBool::new(false);
// Thread A
dados.store(42, Ordering::Relaxed);
pronto.store(true, Ordering::Relaxed); // ERRADO! Pode ser reordenado
// Thread B pode ver pronto=true mas dados=0
// porque Relaxed não garante ordem entre operações diferentes
}
// CORRETO: usar Release/Acquire
fn exemplo_correto() {
let dados = AtomicI32::new(0);
let pronto = AtomicBool::new(false);
// Thread A
dados.store(42, Ordering::Relaxed); // OK: será "empurrado" pelo Release abaixo
pronto.store(true, Ordering::Release); // Garante que escritas anteriores são visíveis
// Thread B
if pronto.load(Ordering::Acquire) {
// Agora dados.load() é garantido retornar 42
let _ = dados.load(Ordering::Relaxed);
}
}
fn main() {
exemplo_errado();
exemplo_correto();
}
Garantias de Thread Safety
- Todos os tipos atômicos são
Send + Sync— podem ser compartilhados livremente entre threads. - Operações atômicas são indivisíveis: nenhuma thread vê um valor “pela metade”.
Orderingcontrola apenas a visibilidade e reordenação de outras operações de memória ao redor da operação atômica.- Na prática em x86/x86_64,
RelaxedeSeqCsttêm performance similar para loads. Em ARM, a diferença é mais significativa. - Atômicos não são um substituto para
Mutexquando você precisa fazer múltiplas operações como uma transação.
Veja Também
- Mutex e RwLock em Rust — quando atômicos não bastam
- Once, OnceLock e LazyLock — inicialização global com atômicos
- Send e Sync — por que atômicos são Sync
- Padrões de Thread Safety — quando usar cada primitiva
- Documentação oficial:
std::sync::atomic