std.atomic em Zig — Referência e Exemplos

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

OrderingUsoExemplo
.relaxedContadores isoladosEstatísticas
.acquireLeitura de flag/lockwhile (flag.load(.acquire))
.releaseEscrita de flag/lockflag.store(true, .release)
.acq_relRead-modify-writefetchAdd, swap
.seq_cstQuando em dúvidaCorreto mas mais lento

Módulos Relacionados

Tutoriais e Receitas Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.