Tipos Atômicos em Rust: std::sync::atomic

Guia completo de tipos atômicos em Rust: AtomicBool, AtomicUsize, Ordering, compare_and_swap, fetch_add e memory ordering em português.

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 Mutex ou RwLock.
  • Não entende as implicações de memory ordering — use Mutex que é mais simples e seguro.

Tipos e Funções Principais

TipoDescrição
AtomicBoolBooleano atômico
AtomicI8/I16/I32/I64Inteiros com sinal atômicos
AtomicU8/U16/U32/U64Inteiros sem sinal atômicos
AtomicUsize / AtomicIsizeInteiros do tamanho de ponteiro atômicos
AtomicPtr<T>Ponteiro atômico

Operações comuns

MétodoDescriçã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)
OrderingQuando usar
RelaxedContadores simples onde a ordem não importa
AcquireLeitura que precisa ver os dados escritos antes de um Release
ReleaseEscrita que publica dados para quem fizer Acquire
AcqRelOperações de leitura+escrita (como compare_exchange)
SeqCstQuando 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”.
  • Ordering controla 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, Relaxed e SeqCst têm performance similar para loads. Em ARM, a diferença é mais significativa.
  • Atômicos não são um substituto para Mutex quando você precisa fazer múltiplas operações como uma transação.

Veja Também