---
title: "Mutex vs Atomic em Zig: Quando Usar Cada Um"
url: "https://ziglang.com.br/artigos/zig-mutex-vs-atomic-quando-usar/"
markdown_url: "https://ziglang.com.br/artigos/zig-mutex-vs-atomic-quando-usar.MD"
description: "Compare std.Thread.Mutex e std.atomic.Value em Zig. Veja critérios práticos, memory ordering, contenção, invariantes, benchmarks e erros comuns."
date: "2026-09-06"
author: ""
---

# Mutex vs Atomic em Zig: Quando Usar Cada Um

Compare std.Thread.Mutex e std.atomic.Value em Zig. Veja critérios práticos, memory ordering, contenção, invariantes, benchmarks e erros comuns.


Para escolher entre **mutex e atomic em Zig**, use esta regra: se você protege uma **invariável com vários campos ou vários passos**, escolha `std.Thread.Mutex`; se atualiza um **valor pequeno e independente** com uma única operação, considere `std.atomic.Value`. Contadores, flags e IDs são bons candidatos a atomic. Mapas, filas, caches e transições compostas normalmente pedem mutex.

Atomic não é um “mutex mais rápido”. Ele transfere a complexidade do bloqueio para um protocolo de memória que precisa continuar correto em todas as interleavings possíveis. Um mutex simples costuma vencer em legibilidade, manutenção e até desempenho quando permite agrupar trabalho e reduzir disputa na mesma linha de cache.

Este guia compara os dois mecanismos, mostra exemplos seguros, explica `monotonic`, `acquire` e `release` sem jargão desnecessário e oferece um roteiro de decisão para código de produção.

## Resposta rápida: mutex ou atomic?

| Situação | Escolha inicial | Motivo |
|---|---|---|
| Contador de requisições | Atomic | Uma atualização independente com `fetchAdd` |
| Flag de shutdown | Atomic | Um booleano publicado entre threads |
| Gerador de IDs | Atomic | Incremento indivisível, sem invariável externa |
| Saldo + limite + histórico | Mutex | Vários valores precisam mudar juntos |
| `HashMap` compartilhado | Mutex | A operação altera estrutura interna composta |
| Fila produtor-consumidor | Mutex + Condition | Estado composto e espera eficiente |
| Estatísticas com muitos campos | Mutex ou dados por thread | Snapshot consistente e menos contenção |
| Estrutura lock-free especializada | Atomic, após prova e benchmark | O protocolo inteiro depende de ordenação de memória |

A coluna diz **escolha inicial** porque o perfil da carga importa. Comece com a solução mais fácil de provar correta. Só troque depois de medir contenção real com um benchmark que represente número de threads, taxa de escrita, arquitetura e tamanho dos dados da aplicação.

## O que um Mutex garante

Um mutex dá exclusividade sobre uma região crítica. Enquanto uma thread mantém o lock, nenhuma outra que respeite o mesmo mutex entra naquela região.

```zig
const std = @import("std");

const Account = struct {
    balance: i64 = 0,
    reserved: i64 = 0,
    mutex: std.Thread.Mutex = .{},

    pub fn reserve(self: *Account, amount: i64) !void {
        self.mutex.lock();
        defer self.mutex.unlock();

        const available = self.balance - self.reserved;
        if (amount < 0 or amount > available) {
            return error.InsufficientFunds;
        }

        self.reserved += amount;
    }

    pub fn snapshot(self: *Account) struct { balance: i64, reserved: i64 } {
        self.mutex.lock();
        defer self.mutex.unlock();

        return .{
            .balance = self.balance,
            .reserved = self.reserved,
        };
    }
};
```

O lock protege mais do que duas variáveis. Ele protege a regra `reserved <= balance` e a sequência “calcular disponível, validar, atualizar”. Transformar `balance` e `reserved` em atomics separados impediria data races em cada inteiro, mas não garantiria que outra thread observasse uma combinação válida entre eles.

Esse é o principal critério: **atomicidade de campo não equivale a atomicidade da operação de negócio**.

Mutex também comunica intenção. Durante code review, o leitor identifica o início e o fim da região crítica. O desafio passa a ser verificar se todos os acessos usam o mesmo lock, se a ordem entre locks evita deadlock e se o trabalho caro acontece fora da região protegida.

## O que um Atomic garante

`std.atomic.Value(T)` oferece operações indivisíveis sobre tipos compatíveis. Duas threads podem chamar `fetchAdd` no mesmo contador sem perder incrementos.

```zig
const std = @import("std");

const Metrics = struct {
    requests: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
    failures: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),

    pub fn record(self: *Metrics, failed: bool) void {
        _ = self.requests.fetchAdd(1, .monotonic);
        if (failed) {
            _ = self.failures.fetchAdd(1, .monotonic);
        }
    }
};
```

Aqui cada contador é independente. Um painel pode observar `requests = 100` e `failures = 4`, depois `requests = 101` e ainda `failures = 4` por um instante. Isso é aceitável para telemetria aproximada. Se a aplicação exige um snapshot em que todos os campos correspondam exatamente ao mesmo evento, atomics separados não bastam.

Atomic garante que a operação no valor não seja rasgada nem perdida. A **memory ordering** define como essa operação se relaciona com outros acessos à memória.

## Memory ordering em linguagem prática

A ordenação não é uma escolha de “quanto quero que seja atômico”. A operação já é atômica. A escolha responde: **quais outras escritas de uma thread precisam ficar visíveis para outra thread quando ela observa este valor?**

### `monotonic`: contador sem mensagem escondida

Use `monotonic` quando o valor só representa a si mesmo. Um contador de chamadas é o exemplo clássico:

```zig
_ = metrics.requests.fetchAdd(1, .monotonic);
```

A aplicação precisa de incrementos indivisíveis, mas observar o número `42` não promete que algum outro buffer ou objeto está pronto.

### `release` e `acquire`: publicar dados prontos

Considere uma thread que preenche uma configuração e depois marca `ready = true`. Outra espera a flag e então lê a configuração.

```zig
const std = @import("std");

const Shared = struct {
    config: Config = undefined,
    ready: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
};

const Config = struct {
    port: u16,
    workers: usize,
};

fn publish(shared: *Shared) void {
    shared.config = .{ .port = 8080, .workers = 4 };
    shared.ready.store(true, .release);
}

fn consume(shared: *Shared) ?Config {
    if (!shared.ready.load(.acquire)) return null;
    return shared.config;
}
```

O `store(.release)` publica as escritas anteriores. O `load(.acquire)` que observa `true` passa a enxergar a configuração inicializada antes da publicação.

Esse padrão é correto apenas porque existe **um escritor antes da publicação** e os leitores não alteram `config`. Se a configuração puder ser substituída enquanto leitores a usam, surgem problemas de ownership, tempo de vida e recuperação de memória. Nesse caso, um mutex ou um desenho de snapshots imutáveis costuma ser mais seguro.

### Ordem forte não conserta protocolo errado

Escolher a ordem mais forte disponível pode simplificar o raciocínio inicial, mas não transforma vários atomics em uma transação. Também não resolve ponteiro liberado cedo demais, acesso não atômico concorrente ao mesmo endereço ou uma invariável dividida entre campos.

Quando a explicação da ordem de memória começa a depender de “provavelmente”, recue. Use mutex, escreva testes de estresse e só retome um protocolo lock-free com uma especificação clara.

## Exemplo clássico: flag de parada

Uma flag de shutdown é um bom uso de atomic porque tem um único significado e não exige bloquear uma thread apenas para consultar um booleano.

```zig
const std = @import("std");

const Worker = struct {
    stopping: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),

    pub fn run(self: *Worker) void {
        while (!self.stopping.load(.acquire)) {
            processOneBatch();
        }
    }

    pub fn stop(self: *Worker) void {
        self.stopping.store(true, .release);
    }
};

fn processOneBatch() void {
    // Trabalho limitado: o loop precisa voltar a consultar stopping.
}
```

O detalhe operacional é importante: atomic não acorda uma thread bloqueada em leitura de socket, `Condition.wait` ou outra chamada demorada. Ele só torna a flag consultável com segurança. Para workers que dormem esperando fila, combine estado protegido e notificação como no guia de [fila limitada com backpressure](/artigos/zig-fila-limitada-mutex-condition-backpressure/). Para serviços, veja também [graceful shutdown com SIGTERM e SIGINT](/artigos/zig-graceful-shutdown-sigterm-sigint/).

## Por que atomic pode ficar lento sob contenção

Um contador atômico compartilhado parece mínimo: uma variável e uma instrução. Mas cada escrita precisa obter controle exclusivo da linha de cache que contém o valor. Com muitas threads em CPUs diferentes, essa linha viaja entre caches repetidamente.

O resultado possível é um **hot spot de coerência**. Adicionar threads aumenta a disputa em vez de aumentar throughput. As alternativas incluem:

- um contador por thread ou por shard, somado apenas na leitura;
- acumular localmente e publicar em lotes;
- proteger várias atualizações relacionadas com um único mutex;
- reduzir a frequência da métrica;
- aceitar amostragem quando precisão total não é necessária.

Um mutex sem contenção também pode ter caminho rápido barato. E, quando existe contenção, o sistema operacional pode estacionar threads em vez de deixá-las competir continuamente pela linha de cache. Portanto, “não bloqueia” não significa “não espera” nem “usa menos CPU”.

## Compare-and-swap: útil, mas não gratuito

Algoritmos lock-free frequentemente usam compare-and-swap: ler um estado, calcular o próximo e tentar publicar apenas se ninguém mudou o valor. Se a tentativa falha, o loop recomeça.

O padrão conceitual é:

```zig
var current = value.load(.acquire);
while (true) {
    const next = calculate(current);

    if (tryCompareExchange(&value, current, next)) {
        break;
    } else |observed| {
        current = observed;
    }
}
```

Os nomes e assinaturas exatos das operações de compare-and-swap podem mudar entre versões do Zig; confira a API da versão fixada no projeto. O ponto arquitetural permanece: sob disputa, a função `calculate` pode rodar várias vezes, e uma thread específica pode perder repetidamente.

Também aparecem problemas que um contador simples não mostra:

- **ABA**: o valor muda de A para B e volta a A, enganando uma comparação;
- **reclamation**: quando é seguro liberar um nó que outros leitores talvez ainda alcancem?;
- **starvation**: uma thread progride, outra repete indefinidamente;
- **falsa partilha**: atomics independentes ocupam a mesma linha de cache;
- **portabilidade**: custo e suporte variam por tipo e arquitetura.

Se você precisa de uma fila comum, comece com mutex e condition. Só implemente fila lock-free quando o perfil provar que o lock é o gargalo e o contrato SPSC, MPSC ou MPMC estiver definido. A referência de [`std.atomic`](/stdlib/std-atomic/) ajuda com as operações, mas não substitui a prova do algoritmo.

## Como decidir em cinco perguntas

### 1. Quantos valores formam a invariável?

Um valor independente favorece atomic. Dois ou mais campos que precisam concordar favorecem mutex.

### 2. A operação cabe em uma primitiva atômica?

`fetchAdd`, `store`, `load` e `swap` são bons sinais. “Consultar mapa, validar, alocar, inserir e atualizar LRU” é uma região crítica, não uma operação atômica simples.

### 3. Leitores precisam de snapshot consistente?

Se sim, mutex é a escolha direta. Para evitar lock em leitura, snapshots imutáveis podem funcionar, mas exigem uma estratégia segura para publicar e liberar versões antigas.

### 4. Existe espera real ou só uma hipótese de performance?

Sem medição, escolha clareza. Instrumente tempo esperando lock, throughput e latência de cauda antes de reescrever.

### 5. A equipe consegue explicar a memory ordering?

A explicação precisa nomear a escrita publicada, a operação `release`, a operação `acquire` correspondente e o tempo de vida dos dados. Se isso não cabe em um comentário curto e verificável, o protocolo está complexo demais para o benefício esperado.

## Benchmark correto para mutex vs atomic

Um microbenchmark que apenas incrementa um valor em um loop mede o pior caso de contenção artificial. Ele é útil, mas não responde sozinho qual desenho funciona na aplicação.

Teste pelo menos estas dimensões:

1. uma, duas, quatro e tantas threads quanto CPUs relevantes;
2. leituras raras versus frequentes;
3. quantidade real de trabalho entre atualizações;
4. contador único versus sharding;
5. p50, p95 e p99, não apenas operações por segundo;
6. build `ReleaseFast` e, quando segurança importa, `ReleaseSafe`;
7. a arquitetura de produção, porque x86-64 e ARM não têm custos idênticos.

Evite otimizar uma primitiva isolada enquanto o sistema passa a maior parte do tempo em alocação, I/O ou parsing. Use as técnicas do guia de [profiling com perf e flamegraph](/artigos/zig-profiling-perf-flamegraph/) para localizar o gargalo antes de trocar o modelo de sincronização.

## Erros comuns

**Usar `volatile` para sincronizar threads.** `volatile` não substitui atomic nem mutex. Ele serve a acessos cujo efeito externo precisa ser preservado, como memória mapeada de dispositivo; não cria o protocolo de sincronização entre threads.

**Misturar acesso atômico e não atômico ao mesmo valor.** Se um endereço é compartilhado e pode ser escrito concorrentemente, todos os acessos relevantes precisam seguir o mesmo protocolo.

**Proteger a escrita, mas não a leitura.** Ler uma struct sem lock enquanto outra thread a altera continua errado, mesmo que o escritor use mutex perfeitamente.

**Segurar mutex durante I/O.** Copie ou retire o estado necessário, solte o lock e só então faça disco, rede, compressão ou callback de usuário.

**Usar spin lock para espera longa.** Spin consome CPU enquanto espera. Ele só faz sentido em contextos muito específicos, com regiões minúsculas e medição cuidadosa.

**Confundir lock-free com simples.** Uma implementação curta pode esconder requisitos complexos de ordenação, ownership e progresso.

## Checklist para code review

Antes de aprovar código concorrente em Zig, confirme:

- [ ] cada variável compartilhada tem um protocolo explícito;
- [ ] a invariável protegida pelo mutex está documentada;
- [ ] todos os acessos à invariável usam o mesmo lock;
- [ ] a região crítica não faz I/O nem chama código arbitrário;
- [ ] atomics representam estado pequeno e independente;
- [ ] `monotonic`, `acquire` e `release` têm justificativa concreta;
- [ ] o tempo de vida dos dados publicados é maior que o dos leitores;
- [ ] shutdown acorda threads bloqueadas, não apenas muda uma flag;
- [ ] o benchmark reproduz a contenção de produção;
- [ ] a versão do Zig está fixada no build e no CI.

## Conclusão

Na maioria dos projetos, a escolha segura é simples: **mutex para estado composto; atomic para estado independente**. Comece por correção e legibilidade. Um lock visível em torno de uma invariável é mais fácil de testar, revisar e manter do que um protocolo lock-free cujo benefício ainda não foi medido.

Use atomic para contadores, flags e IDs quando a semântica couber em uma operação. Use mutex para mapas, filas, snapshots consistentes e transações entre campos. Se a contenção aparecer no profiler, reduza a região crítica, faça sharding ou mova trabalho para fora do lock antes de partir para compare-and-swap.

Para continuar, leia [concorrência avançada em Zig](/artigos/zig-concorrencia-padroes-avancados/) e o tutorial de [threads e concorrência](/tutoriais/concorrencia-em-zig/). A melhor primitiva não é a que parece mais sofisticada: é a que mantém o contrato correto sob carga e ainda pode ser explicada pela equipe seis meses depois.

## Perguntas frequentes

### Atomic é sempre mais rápido que Mutex?

Não. Um atomic muito disputado pode gerar tráfego intenso de coerência de cache. Mutex pode ter caminho rápido sem contenção e permite agrupar operações. Meça o padrão real.

### Posso usar atomic para proteger uma struct inteira?

Só quando o tipo e o alvo suportam a operação e toda a invariável cabe naquele valor. Na prática, structs mutáveis com vários campos normalmente ficam mais claras e portáveis sob mutex.

### `monotonic` é seguro para contador?

Sim, quando o contador só representa o próprio número e não publica a disponibilidade de outros dados. Se observar o valor deve tornar outras escritas visíveis, você precisa de um protocolo como release/acquire.

### Uma flag atomic acorda uma thread bloqueada?

Não. A flag só pode ser observada quando a thread volta a executar e faz `load`. Para espera bloqueante, use `Condition`, event loop, cancelamento da operação de I/O ou outro mecanismo de notificação.

### Quando vale implementar lock-free?

Quando um profiler mostra que sincronização é gargalo, o padrão de produtores e consumidores está bem definido, a equipe consegue provar ordenação e tempo de vida, e benchmarks mostram ganho sustentável na arquitetura de produção.
