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ção | Descriçã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
| Ordering | Descrição |
|---|---|
.relaxed | Sem garantia de ordem (mais rápido) |
.acquire | Leituras após esta veem escritas anteriores |
.release | Escritas antes desta são visíveis após acquire |
.acq_rel | Acquire + Release combinados |
.seq_cst | Mais 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
- Allocators — Alocadores thread-safe
- Error Handling — Erros em contexto concorrente
- Producer-Consumer Pattern — Padrão produtor-consumidor
- Pool de Objetos — Pool thread-safe
- FAQ Performance — Quando usar concorrência