std.atomic — Operações Atômicas
O módulo std.atomic fornece operações atômicas para programação concorrente sem locks (lock-free programming). Operações atômicas são instruções que o hardware garante que serão executadas de forma indivisível, mesmo quando múltiplas threads acessam a mesma posição de memória simultaneamente. Elas são a base para construir estruturas de dados concorrentes de alto desempenho.
Visão Geral
const std = @import("std");
const atomic = std.atomic;
Tipo Principal: atomic.Value
pub fn Value(comptime T: type) type {
return struct {
raw: T,
pub fn init(value: T) Value(T)
pub fn load(self: *const Self, order: Order) T
pub fn store(self: *Self, value: T, order: Order) void
pub fn swap(self: *Self, value: T, order: Order) T
pub fn fetchAdd(self: *Self, value: T, order: Order) T
pub fn fetchSub(self: *Self, value: T, order: Order) T
pub fn fetchAnd(self: *Self, value: T, order: Order) T
pub fn fetchOr(self: *Self, value: T, order: Order) T
pub fn compareAndSwap(self: *Self, expected: T, desired: T, success: Order, failure: Order) ?T
};
}
Ordenação de Memória (Memory Ordering)
pub const Order = enum {
relaxed, // Sem garantias de ordenação
acquire, // Garante visibilidade de escritas anteriores
release, // Garante que escritas são visíveis após esta operação
acq_rel, // Acquire + Release
seq_cst, // Sequencialmente consistente (mais forte)
monotonic, // Alias para relaxed
};
Exemplo 1: Contadores Atômicos
const std = @import("std");
var contador_global = std.atomic.Value(u64).init(0);
var max_global = std.atomic.Value(u64).init(0);
fn trabalho(id: u64) void {
for (0..100_000) |_| {
// Incrementa atomicamente
const anterior = contador_global.fetchAdd(1, .seq_cst);
// Atualiza máximo de forma atômica (compare-and-swap loop)
var atual_max = max_global.load(.seq_cst);
while (anterior + 1 > atual_max) {
if (max_global.compareAndSwap(atual_max, anterior + 1, .seq_cst, .seq_cst)) |real| {
atual_max = real;
} else {
break;
}
}
}
_ = id;
}
pub fn main() !void {
const NUM_THREADS = 8;
var threads: [NUM_THREADS]std.Thread = undefined;
for (0..NUM_THREADS) |i| {
threads[i] = try std.Thread.spawn(.{}, trabalho, .{@as(u64, i)});
}
for (threads) |t| t.join();
const stdout = std.io.getStdOut().writer();
try stdout.print("Contador final: {d}\n", .{contador_global.load(.seq_cst)});
try stdout.print("Esperado: {d}\n", .{NUM_THREADS * 100_000});
try stdout.print("Máximo atingido: {d}\n", .{max_global.load(.seq_cst)});
}
Exemplo 2: Flag de Parada e Spin Lock
const std = @import("std");
const SpinLock = struct {
locked: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
pub fn acquire(self: *SpinLock) void {
while (self.locked.swap(1, .acquire) != 0) {
// Spin: espera ativamente
std.atomic.spinLoopHint();
}
}
pub fn release(self: *SpinLock) void {
self.locked.store(0, .release);
}
};
var lock = SpinLock{};
var dado_compartilhado: u64 = 0;
var parar = std.atomic.Value(bool).init(false);
fn trabalhador() void {
var local: u64 = 0;
while (!parar.load(.acquire)) {
lock.acquire();
dado_compartilhado += 1;
local += 1;
lock.release();
}
std.debug.print("Thread processou {d} iterações\n", .{local});
}
pub fn main() !void {
var threads: [4]std.Thread = undefined;
for (0..4) |i| {
threads[i] = try std.Thread.spawn(.{}, trabalhador, .{});
}
// Deixa rodar por 100ms
std.time.sleep(100 * std.time.ns_per_ms);
// Sinaliza parada
parar.store(true, .release);
for (threads) |t| t.join();
const stdout = std.io.getStdOut().writer();
try stdout.print("Total de incrementos: {d}\n", .{dado_compartilhado});
}
Exemplo 3: Fila SPSC (Single Producer, Single Consumer) Lock-Free
const std = @import("std");
fn SpscQueue(comptime T: type, comptime N: usize) type {
return struct {
buffer: [N]T = undefined,
head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
tail: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
const Self = @This();
pub fn push(self: *Self, item: T) bool {
const tail = self.tail.load(.relaxed);
const next_tail = (tail + 1) % N;
if (next_tail == self.head.load(.acquire)) {
return false; // fila cheia
}
self.buffer[tail] = item;
self.tail.store(next_tail, .release);
return true;
}
pub fn pop(self: *Self) ?T {
const head = self.head.load(.relaxed);
if (head == self.tail.load(.acquire)) {
return null; // fila vazia
}
const item = self.buffer[head];
self.head.store((head + 1) % N, .release);
return item;
}
};
}
var fila = SpscQueue(i32, 1024){};
var producao_completa = std.atomic.Value(bool).init(false);
fn produtor() void {
for (0..1000) |i| {
while (!fila.push(@intCast(i))) {
std.atomic.spinLoopHint();
}
}
producao_completa.store(true, .release);
}
fn consumidor() void {
var soma: i64 = 0;
var count: u32 = 0;
while (true) {
if (fila.pop()) |item| {
soma += item;
count += 1;
} else if (producao_completa.load(.acquire)) {
// Drena itens restantes
while (fila.pop()) |item| {
soma += item;
count += 1;
}
break;
} else {
std.atomic.spinLoopHint();
}
}
std.debug.print("Consumiu {d} itens, soma = {d}\n", .{ count, soma });
}
pub fn main() !void {
const t_prod = try std.Thread.spawn(.{}, produtor, .{});
const t_cons = try std.Thread.spawn(.{}, consumidor, .{});
t_prod.join();
t_cons.join();
// Verificação: soma de 0..999 = 499500
const stdout = std.io.getStdOut().writer();
try stdout.print("Soma esperada: {d}\n", .{@as(i64, 999) * 1000 / 2});
}
Guia de Memory Ordering
| Ordering | Uso | Exemplo |
|---|---|---|
.relaxed | Contadores isolados | Estatísticas |
.acquire | Leitura de flag/lock | while (flag.load(.acquire)) |
.release | Escrita de flag/lock | flag.store(true, .release) |
.acq_rel | Read-modify-write | fetchAdd, swap |
.seq_cst | Quando em dúvida | Correto mas mais lento |
Módulos Relacionados
- std.Thread — Threads nativas
- std.Thread.Mutex — Exclusão mútua (quando lock-free não é necessário)
- std.Thread.Pool — Pool de threads