Cheatsheet: Concorrência em Zig

Cheatsheet: Concorrência em Zig

Zig oferece primitivas de concorrência de baixo nível na biblioteca padrão, dando controle direto sobre threads, mutexes e operações atômicas. Diferente de linguagens com runtimes pesados (Go, Erlang), Zig não tem um scheduler embutido — você trabalha diretamente com threads do sistema operacional, mantendo a filosofia de zero overhead e controle total.

Criação de Threads

Thread simples

const std = @import("std");

fn tarefa(id: usize) void {
    std.debug.print("Thread {d} executando\n", .{id});
}

pub fn main() !void {
    // Criar e iniciar thread
    const thread = try std.Thread.spawn(.{}, tarefa, .{1});

    // Fazer algo na thread principal...
    std.debug.print("Thread principal\n", .{});

    // Esperar a thread terminar
    thread.join();
}

Múltiplas threads

const std = @import("std");

fn trabalho(id: usize) void {
    std.debug.print("Worker {d} iniciou\n", .{id});
    // simular trabalho
    std.time.sleep(100 * std.time.ns_per_ms);
    std.debug.print("Worker {d} terminou\n", .{id});
}

pub fn main() !void {
    const NUM_THREADS = 4;
    var threads: [NUM_THREADS]std.Thread = undefined;

    // Iniciar todas as threads
    for (0..NUM_THREADS) |i| {
        threads[i] = try std.Thread.spawn(.{}, trabalho, .{i});
    }

    // Esperar todas terminarem
    for (threads) |t| {
        t.join();
    }

    std.debug.print("Todas as threads terminaram\n", .{});
}

Thread com retorno via ponteiro

const std = @import("std");

fn calcular(resultado: *i64) void {
    var soma: i64 = 0;
    for (0..1000) |i| {
        soma += @intCast(i);
    }
    resultado.* = soma;
}

pub fn main() !void {
    var resultado: i64 = 0;

    const thread = try std.Thread.spawn(.{}, calcular, .{&resultado});
    thread.join();

    std.debug.print("Resultado: {d}\n", .{resultado});
}

Mutex

Mutex básico para proteção de dados

const std = @import("std");

const ContadorSeguro = struct {
    valor: i64 = 0,
    mutex: std.Thread.Mutex = .{},

    fn incrementar(self: *ContadorSeguro) void {
        self.mutex.lock();
        defer self.mutex.unlock();
        self.valor += 1;
    }

    fn ler(self: *ContadorSeguro) i64 {
        self.mutex.lock();
        defer self.mutex.unlock();
        return self.valor;
    }
};

pub fn main() !void {
    var contador = ContadorSeguro{};
    const NUM_THREADS = 8;
    const INCREMENTOS = 10000;
    var threads: [NUM_THREADS]std.Thread = undefined;

    for (0..NUM_THREADS) |i| {
        threads[i] = try std.Thread.spawn(.{}, struct {
            fn run(c: *ContadorSeguro) void {
                for (0..INCREMENTOS) |_| {
                    c.incrementar();
                }
            }
        }.run, .{&contador});
    }

    for (threads) |t| t.join();

    std.debug.print("Contador: {d} (esperado: {d})\n", .{
        contador.ler(),
        NUM_THREADS * INCREMENTOS,
    });
}

Operações Atômicas

Para operações simples em dados compartilhados, atômicos são mais eficientes que mutex:

const std = @import("std");

pub fn main() !void {
    var contador = std.atomic.Value(i64).init(0);
    const NUM_THREADS = 8;
    var threads: [NUM_THREADS]std.Thread = undefined;

    for (0..NUM_THREADS) |i| {
        threads[i] = try std.Thread.spawn(.{}, struct {
            fn run(c: *std.atomic.Value(i64)) void {
                for (0..10000) |_| {
                    _ = c.fetchAdd(1, .seq_cst);
                }
            }
        }.run, .{&contador});
    }

    for (threads) |t| t.join();

    std.debug.print("Contador: {d}\n", .{contador.load(.seq_cst)});
}

Operações atômicas disponíveis

OperaçãoDescrição
load(order)Ler valor atomicamente
store(val, order)Escrever valor atomicamente
fetchAdd(val, order)Soma atômica, retorna valor anterior
fetchSub(val, order)Subtração atômica
fetchAnd(val, order)AND atômico
fetchOr(val, order)OR atômico
cmpxchgWeak(expected, new, succ_order, fail_order)Compare-and-swap fraco
cmpxchgStrong(expected, new, succ_order, fail_order)Compare-and-swap forte

Ordering de memória

OrderingDescrição
.relaxedSem garantia de ordem (mais rápido)
.acquireLeituras após esta veem escritas anteriores
.releaseEscritas antes desta são visíveis após acquire
.acq_relAcquire + Release combinados
.seq_cstMais forte — ordem total sequencial (mais seguro)

Thread Pool

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var pool: std.Thread.Pool = undefined;
    try pool.init(.{
        .allocator = gpa.allocator(),
        .n_jobs = 4, // número de threads no pool
    });
    defer pool.deinit();

    // Agendar trabalho no pool
    for (0..20) |i| {
        pool.spawn(struct {
            fn work(id: usize) void {
                std.debug.print("Tarefa {d} executando\n", .{id});
            }
        }.work, .{i});
    }

    // Pool é finalizado no defer, esperando todas as tarefas
}

Condition Variables

const std = @import("std");

const FilaSegura = struct {
    dados: [100]i32 = undefined,
    tamanho: usize = 0,
    mutex: std.Thread.Mutex = .{},
    nao_vazio: std.Thread.Condition = .{},
    nao_cheio: std.Thread.Condition = .{},

    fn inserir(self: *FilaSegura, valor: i32) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        while (self.tamanho >= 100) {
            self.nao_cheio.wait(&self.mutex);
        }

        self.dados[self.tamanho] = valor;
        self.tamanho += 1;
        self.nao_vazio.signal();
    }

    fn remover(self: *FilaSegura) i32 {
        self.mutex.lock();
        defer self.mutex.unlock();

        while (self.tamanho == 0) {
            self.nao_vazio.wait(&self.mutex);
        }

        self.tamanho -= 1;
        const valor = self.dados[self.tamanho];
        self.nao_cheio.signal();
        return valor;
    }
};

Padrões Comuns

Dados por thread (sem compartilhamento)

const std = @import("std");

fn processarParte(inicio: usize, fim: usize, resultado: *i64) void {
    var soma: i64 = 0;
    for (inicio..fim) |i| {
        soma += @intCast(i);
    }
    resultado.* = soma;
}

pub fn main() !void {
    const TOTAL = 1_000_000;
    const NUM_THREADS = 4;
    const PARTE = TOTAL / NUM_THREADS;

    var resultados: [NUM_THREADS]i64 = undefined;
    var threads: [NUM_THREADS]std.Thread = undefined;

    for (0..NUM_THREADS) |i| {
        threads[i] = try std.Thread.spawn(
            .{},
            processarParte,
            .{ i * PARTE, (i + 1) * PARTE, &resultados[i] },
        );
    }

    for (threads) |t| t.join();

    var total: i64 = 0;
    for (resultados) |r| total += r;

    std.debug.print("Soma total: {d}\n", .{total});
}

Once — Inicialização única thread-safe

const std = @import("std");

var recurso_global: ?*Recurso = null;
var once = std.once(inicializarRecurso);

fn inicializarRecurso() void {
    // Executado apenas uma vez, mesmo com múltiplas threads
    recurso_global = criarRecurso();
}

fn obterRecurso() *Recurso {
    once.call();
    return recurso_global.?;
}

Temporizadores e Sleep

const std = @import("std");

pub fn main() void {
    // Dormir por tempo específico
    std.time.sleep(1 * std.time.ns_per_s);    // 1 segundo
    std.time.sleep(500 * std.time.ns_per_ms);  // 500 milissegundos

    // Medir tempo
    var timer = std.time.Timer.start() catch unreachable;
    // ... operação ...
    const elapsed_ns = timer.read();
    std.debug.print("Tempo: {d}ms\n", .{elapsed_ns / std.time.ns_per_ms});

    // Timestamp
    const timestamp = std.time.timestamp();
    std.debug.print("Timestamp: {d}\n", .{timestamp});
}

Erros Comuns

// ERRO: Acessar dados compartilhados sem proteção
// var dados: i32 = 0;
// Thread 1: dados += 1;  // data race!
// Thread 2: dados += 1;

// CORRETO: Usar mutex ou atômico
var mutex = std.Thread.Mutex{};
mutex.lock();
defer mutex.unlock();
// dados += 1;

// ERRO: Deadlock — dois mutexes em ordem diferente
// Thread 1: lock(A) -> lock(B)
// Thread 2: lock(B) -> lock(A)  // DEADLOCK!

// CORRETO: Sempre trancar na mesma ordem

Veja Também

Continue aprendendo Zig

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