Introdução
Operações atômicas são instruções de CPU que executam de forma indivisível, sem possibilidade de interrupção por outra thread. Elas permitem sincronização entre threads sem o overhead de mutexes, sendo ideais para contadores, flags e estruturas lock-free.
Em Zig, std.atomic.Value fornece uma API segura para operações atômicas.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento de threads e mutex
Contador Atômico
O uso mais comum: um contador seguro entre threads sem mutex:
const std = @import("std");
pub fn main() !void {
var contador = std.atomic.Value(u64).init(0);
const num_threads = 8;
var threads: [num_threads]std.Thread = undefined;
for (&threads) |*t| {
t.* = try std.Thread.spawn(.{}, struct {
fn work(c: *std.atomic.Value(u64)) void {
for (0..10000) |_| {
_ = c.fetchAdd(1, .seq_cst);
}
}
}.work, .{&contador});
}
for (&threads) |t| {
t.join();
}
const total = contador.load(.seq_cst);
std.debug.print("Total: {d} (esperado: {d})\n", .{ total, num_threads * 10000 });
}
Saída esperada
Total: 80000 (esperado: 80000)
Operações Atômicas Disponíveis
O Zig oferece diversas operações atômicas:
const std = @import("std");
pub fn main() void {
var valor = std.atomic.Value(i32).init(10);
// Load: ler o valor atual
const atual = valor.load(.seq_cst);
std.debug.print("Valor atual: {d}\n", .{atual});
// Store: definir um novo valor
valor.store(42, .seq_cst);
std.debug.print("Após store(42): {d}\n", .{valor.load(.seq_cst)});
// fetchAdd: somar e retornar o valor anterior
const anterior = valor.fetchAdd(8, .seq_cst);
std.debug.print("fetchAdd(8): anterior={d}, novo={d}\n", .{
anterior,
valor.load(.seq_cst),
});
// fetchSub: subtrair e retornar o valor anterior
const ant_sub = valor.fetchSub(10, .seq_cst);
std.debug.print("fetchSub(10): anterior={d}, novo={d}\n", .{
ant_sub,
valor.load(.seq_cst),
});
// fetchAnd: AND bit a bit
valor.store(0xFF, .seq_cst);
_ = valor.fetchAnd(0x0F, .seq_cst);
std.debug.print("fetchAnd(0x0F) de 0xFF: 0x{X}\n", .{
@as(u32, @intCast(valor.load(.seq_cst))),
});
// fetchOr: OR bit a bit
valor.store(0x0F, .seq_cst);
_ = valor.fetchOr(0xF0, .seq_cst);
std.debug.print("fetchOr(0xF0) de 0x0F: 0x{X}\n", .{
@as(u32, @intCast(valor.load(.seq_cst))),
});
}
Saída esperada
Valor atual: 10
Após store(42): 42
fetchAdd(8): anterior=42, novo=50
fetchSub(10): anterior=50, novo=40
fetchAnd(0x0F) de 0xFF: 0xF
fetchOr(0xF0) de 0x0F: 0xFF
Compare and Exchange
A operação mais poderosa: atualiza o valor apenas se ele for o esperado:
const std = @import("std");
const SpinLock = struct {
locked: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
pub fn lock(self: *SpinLock) void {
while (self.locked.cmpxchgWeak(0, 1, .acquire, .monotonic) != null) {
// Spin: esperar até conseguir o lock
std.atomic.spinLoopHint();
}
}
pub fn unlock(self: *SpinLock) void {
self.locked.store(0, .release);
}
};
pub fn main() !void {
var spin = SpinLock{};
var dados: u64 = 0;
var threads: [4]std.Thread = undefined;
for (&threads) |*t| {
t.* = try std.Thread.spawn(.{}, struct {
fn work(s: *SpinLock, d: *u64) void {
for (0..10000) |_| {
s.lock();
d.* += 1;
s.unlock();
}
}
}.work, .{ &spin, &dados });
}
for (&threads) |t| t.join();
std.debug.print("Dados: {d} (esperado: 40000)\n", .{dados});
}
Flag Atômica para Sinalização
Use uma flag atômica para sinalizar entre threads:
const std = @import("std");
const Flag = struct {
valor: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
pub fn set(self: *Flag) void {
self.valor.store(true, .release);
}
pub fn isSet(self: *Flag) bool {
return self.valor.load(.acquire);
}
pub fn reset(self: *Flag) void {
self.valor.store(false, .release);
}
};
fn produtor(flag: *Flag) void {
std.debug.print("Produtor: preparando dados...\n", .{});
std.time.sleep(std.time.ns_per_s);
std.debug.print("Produtor: dados prontos! Sinalizando.\n", .{});
flag.set();
}
fn consumidor(flag: *Flag) void {
std.debug.print("Consumidor: esperando dados...\n", .{});
while (!flag.isSet()) {
std.atomic.spinLoopHint();
}
std.debug.print("Consumidor: sinal recebido! Processando dados.\n", .{});
}
pub fn main() !void {
var flag = Flag{};
const t1 = try std.Thread.spawn(.{}, consumidor, .{&flag});
const t2 = try std.Thread.spawn(.{}, produtor, .{&flag});
t1.join();
t2.join();
}
Estatísticas Atômicas
Colete estatísticas de forma thread-safe sem mutex:
const std = @import("std");
const Estatisticas = struct {
requisicoes_total: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
requisicoes_ok: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
requisicoes_erro: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
bytes_processados: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
pub fn registrarSucesso(self: *Estatisticas, bytes: u64) void {
_ = self.requisicoes_total.fetchAdd(1, .monotonic);
_ = self.requisicoes_ok.fetchAdd(1, .monotonic);
_ = self.bytes_processados.fetchAdd(bytes, .monotonic);
}
pub fn registrarErro(self: *Estatisticas) void {
_ = self.requisicoes_total.fetchAdd(1, .monotonic);
_ = self.requisicoes_erro.fetchAdd(1, .monotonic);
}
pub fn relatorio(self: *Estatisticas) void {
const total = self.requisicoes_total.load(.seq_cst);
const ok = self.requisicoes_ok.load(.seq_cst);
const erros = self.requisicoes_erro.load(.seq_cst);
const bytes = self.bytes_processados.load(.seq_cst);
std.debug.print("=== Relatório ===\n", .{});
std.debug.print("Total: {d}\n", .{total});
std.debug.print("Sucesso: {d}\n", .{ok});
std.debug.print("Erros: {d}\n", .{erros});
std.debug.print("Bytes: {d}\n", .{bytes});
if (total > 0) {
std.debug.print("Taxa de erro: {d:.1}%\n", .{
@as(f64, @floatFromInt(erros)) / @as(f64, @floatFromInt(total)) * 100.0,
});
}
}
};
fn simularWorker(stats: *Estatisticas, id: u32) void {
var prng = std.Random.DefaultPrng.init(id);
const rand = prng.random();
for (0..1000) |_| {
if (rand.intRangeAtMost(u32, 1, 100) <= 95) {
stats.registrarSucesso(rand.intRangeAtMost(u64, 100, 5000));
} else {
stats.registrarErro();
}
}
}
pub fn main() !void {
var stats = Estatisticas{};
var threads: [8]std.Thread = undefined;
for (&threads, 0..) |*t, i| {
t.* = try std.Thread.spawn(.{}, simularWorker, .{ &stats, @as(u32, @intCast(i)) });
}
for (&threads) |t| t.join();
stats.relatorio();
}
Ordens de Memória
As ordens de memória controlam como operações atômicas são visíveis entre threads:
| Ordem | Descrição | Uso |
|---|---|---|
.monotonic | Sem garantias de ordenação | Contadores, estatísticas |
.acquire | Operações após o load não são reordenadas antes dele | Ler flags/locks |
.release | Operações antes do store não são reordenadas depois dele | Escrever flags/locks |
.seq_cst | Ordem total sequencial | Quando em dúvida |
Dicas e Boas Práticas
Use
.seq_cstquando em dúvida: É a ordem mais segura, embora possa ter pequeno custo de performance.Atômicos para dados simples: Para estruturas complexas, use mutex.
spinLoopHint: Use
std.atomic.spinLoopHint()em loops de espera ativa para economizar energia da CPU.Tamanho importa: Operações atômicas são mais eficientes para tipos de até 8 bytes (64 bits).
Receitas Relacionadas
- Como criar threads em Zig - Fundamentos
- Como usar Mutex em Zig - Alternativa com lock
- Como usar thread pool em Zig - Pool de threads
- Como usar canais de comunicação em Zig - Coordenação