Concorrência é um dos tópicos mais procurados por desenvolvedores Zig, e por um bom motivo: Zig oferece controle total sobre paralelismo, sem a complexidade de Rust nem as restrições de Go. Neste tutorial completo, você vai dominar todos os padrões de concorrência em Zig: desde threads básicas até sistemas de worker pools de produção.
⚠️ Aviso: A API de
async/awaitdo Zig está em evolução. Este tutorial foca nos recursos estáveis:std.Thread, channels e thread pools. Para async, consulte nosso guia dedicado de async/await.
Índice
- Modelo de Concorrência do Zig
- Threads em Zig: O Básico
- Sincronização: Mutex e Condition
- Channels: Comunicação Entre Threads
- Thread Pools: Paralelismo Eficiente
- Padrões de Concorrência Práticos
- Performance e Benchmarks
- Armadilhas Comuns e Como Evitar
- Exercícios Práticos
- FAQ
- Próximos Passos
Modelo de Concorrência do Zig
Antes de escrever código, é importante entender como Zig aborda concorrência:
Zig vs Outras Linguagens
| Aspecto | Zig | Go | Rust | C++ |
|---|---|---|---|---|
| Modelo | Threads explícitas + Async opt-in | Goroutines (green threads) | Ownership + borrowing | std::thread + futures |
| Overhead | Zero (OS threads) | Baixo (~2KB/goroutine) | Zero | Zero |
| Controle | Total | Alto | Alto | Alto |
| Segurança | Manual (com asserts) | GC garante | Compilador garante | Manual |
| Curva de aprendizado | Média | Baixa | Alta | Alta |
Princípios do Zig para Concorrência
- Sem runtime: Zig não tem scheduler embutido — você controla tudo
- Zero overhead: Sem garbage collector, sem green threads
- Composição explícita: Você escolhe o nível de abstração
- Erros tratáveis: Falhas de alocação são
try/catch, não panic
Quando Usar Cada Abordagem
| Cenário | Abordagem Recomendada | Por quê? |
|---|---|---|
| I/O-bound (rede, disco) | Async/await | Menos threads, mais conexões |
| CPU-bound (cálculos) | Thread pool | Usa todos os cores |
| Coordenação simples | Channels | Padrão produtor/consumidor claro |
| Estado compartilhado | Mutex + cond | Controle fino de acesso |
| High-performance server | I/O uring + async | Máxima eficiência |
Threads em Zig: O Básico
A forma mais fundamental de paralelismo em Zig é através de std.Thread.
Criando uma Thread
const std = @import("std");
pub fn main() !void {
// Alocador para a thread
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Criar uma thread
const thread = try std.Thread.spawn(
.{}, // Configuração padrão
workerFunction, // Função a executar
.{42}, // Argumentos (tuple)
);
// Aguardar a thread terminar
thread.join();
std.debug.print("Thread completada!\n", .{});
}
fn workerFunction(id: usize) void {
std.debug.print("Worker {} executando\n", .{id});
std.time.sleep(1 * std.time.ns_per_s); // Simula trabalho
std.debug.print("Worker {} terminou\n", .{id});
}
Saída:
Worker 42 executando
Worker 42 terminou
Thread completada!
Múltiplas Threads
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Criar múltiplas threads
const num_threads = 4;
var threads: [num_threads]std.Thread = undefined;
for (0..num_threads) |i| {
threads[i] = try std.Thread.spawn(
.{},
workerFunction,
.{i},
);
}
// Aguardar todas
for (threads) |thread| {
thread.join();
}
std.debug.print("Todas as {d} threads completadas!\n", .{num_threads});
}
Passando Dados para Threads
const WorkerData = struct {
id: usize,
name: []const u8,
iterations: u32,
};
pub fn main() !void {
const data = WorkerData{
.id = 1,
.name = "Processador de Imagens",
.iterations = 1000,
};
const thread = try std.Thread.spawn(
.{},
processWorker,
.{data}, // Passa por valor (copia)
);
thread.join();
}
fn processWorker(data: WorkerData) void {
std.debug.print(
"Worker {d} ({s}): processando {d} itens\n",
.{data.id, data.name, data.iterations},
);
}
Retornando Resultados (com Channels)
Threads não retornam valores diretamente. Use channels (veja próxima seção) ou estado compartilhado:
const Result = struct {
id: usize,
value: i32,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Array compartilhado para resultados
var results = [_]Result{undefined} ** 4;
var threads: [4]std.Thread = undefined;
for (0..4) |i| {
// Passar ponteiro para resultado específico
threads[i] = try std.Thread.spawn(
.{},
computeAndStore,
.{ i, &results[i] },
);
}
for (threads) |t| t.join();
// Ler resultados
for (results) |r| {
std.debug.print("Resultado {d}: {d}\n", .{r.id, r.value});
}
}
fn computeAndStore(id: usize, result: *Result) void {
// Simula cálculo
const value = @intCast(i32, id * 10);
result.* = .{ .id = id, .value = value };
}
Saída:
Resultado 0: 0
Resultado 1: 10
Resultado 2: 20
Resultado 3: 30
Sincronização: Mutex e Condition
Quando múltiplas threads acessam dados compartilhados, precisamos sincronização.
Mutex Básico
const Counter = struct {
mutex: std.Thread.Mutex,
value: u64,
fn init() Counter {
return .{
.mutex = .{},
.value = 0,
};
}
fn increment(self: *Counter) void {
self.mutex.lock();
defer self.mutex.unlock();
self.value += 1;
}
fn get(self: *Counter) u64 {
self.mutex.lock();
defer self.mutex.unlock();
return self.value;
}
};
pub fn main() !void {
var counter = Counter.init();
var threads: [10]std.Thread = undefined;
for (&threads) |*t| {
t.* = try std.Thread.spawn(
.{},
incrementWorker,
.{&counter},
);
}
for (threads) |t| t.join();
std.debug.print("Contador final: {d}\n", .{counter.get()});
// Sempre imprime: Contador final: 10
}
fn incrementWorker(counter: *Counter) void {
for (0..1000) |_| {
counter.increment();
}
}
RwLock: Leitores e Escritores
Use std.Thread.RwLock quando tiver muitas leituras e poucas escritas:
const Cache = struct {
rwlock: std.Thread.RwLock,
data: std.StringHashMap(i32),
fn get(self: *Cache, key: []const u8) ?i32 {
self.rwlock.lockShared(); // Múltiplos leitores
defer self.rwlock.unlockShared();
return self.data.get(key);
}
fn put(self: *Cache, key: []const u8, value: i32) !void {
self.rwlock.lock(); // Apenas um escritor
defer self.rwlock.unlock();
try self.data.put(key, value);
}
};
Condition Variables
Use std.Thread.Condition para sinalização entre threads:
const Queue = struct {
mutex: std.Thread.Mutex,
cond: std.Thread.Condition,
items: std.ArrayList(i32),
closed: bool,
fn init(allocator: std.mem.Allocator) Queue {
return .{
.mutex = .{},
.cond = .{},
.items = std.ArrayList(i32).init(allocator),
.closed = false,
};
}
fn deinit(self: *Queue) void {
self.items.deinit();
}
fn push(self: *Queue, item: i32) !void {
self.mutex.lock();
defer self.mutex.unlock();
try self.items.append(item);
self.cond.signal(); // Acorda um consumidor
}
fn pop(self: *Queue) ?i32 {
self.mutex.lock();
defer self.mutex.unlock();
// Espera até ter item ou fila fechar
while (self.items.items.len == 0 and !self.closed) {
self.cond.wait(&self.mutex);
}
if (self.items.items.len == 0) return null;
return self.items.orderedRemove(0);
}
fn close(self: *Queue) void {
self.mutex.lock();
defer self.mutex.unlock();
self.closed = true;
self.cond.broadcast(); // Acorda todos
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var queue = Queue.init(allocator);
defer queue.deinit();
// Thread produtora
const producer = try std.Thread.spawn(
.{},
producerFn,
.{&queue},
);
// Thread consumidora
const consumer = try std.Thread.spawn(
.{},
consumerFn,
.{&queue},
);
producer.join();
queue.close(); // Sinaliza fim
consumer.join();
}
fn producerFn(queue: *Queue) !void {
for (0..10) |i| {
try queue.push(@intCast(i32, i));
std.time.sleep(100 * std.time.ns_per_ms);
}
}
fn consumerFn(queue: *Queue) void {
while (queue.pop()) |item| {
std.debug.print("Consumiu: {d}\n", .{item});
}
std.debug.print("Fila fechada\n", .{});
}
Channels: Comunicação Entre Threads
Channels são a forma mais elegante de comunicação entre threads em Zig.
Channel Básico
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Criar channel com buffer de 10 mensagens
var channel = try std.Channel(i32).init(allocator, 10);
defer channel.deinit();
// Thread produtora
const producer = try std.Thread.spawn(
.{},
sendNumbers,
.{&channel},
);
// Thread consumidora
const consumer = try std.Thread.spawn(
.{},
receiveNumbers,
.{&channel},
);
producer.join();
channel.close(); // Fecha para envio
consumer.join();
}
fn sendNumbers(channel: *std.Channel(i32)) !void {
for (0..5) |i| {
try channel.send(@intCast(i32, i));
std.debug.print("Enviou: {d}\n", .{i});
}
}
fn receiveNumbers(channel: *std.Channel(i32)) void {
while (channel.receive()) |num| {
std.debug.print("Recebeu: {d}\n", .{num});
}
}
Saída:
Enviou: 0
Enviou: 1
Recebeu: 0
Recebeu: 1
Enviou: 2
Enviou: 3
Recebeu: 2
...
Select: Múltiplos Channels
fn multiplex(
ch1: *std.Channel(i32),
ch2: *std.Channel(i32),
done: *std.Channel(void),
) !void {
var buf1: i32 = undefined;
var buf2: i32 = undefined;
while (true) {
// select é simulado com polling em Zig
// (API nativa em desenvolvimento)
if (ch1.tryReceive(&buf1)) |val| {
std.debug.print("Ch1: {d}\n", .{val});
} else if (ch2.tryReceive(&buf2)) |val| {
std.debug.print("Ch2: {d}\n", .{val});
} else if (done.tryReceive(null)) |_| {
break; // Sinal de término
} else {
// Pequena pausa para não busy-wait
std.time.sleep(1 * std.time.ns_per_ms);
}
}
}
Fan-Out/Fan-In Pattern
Distribuir trabalho entre workers e coletar resultados:
const Task = struct {
id: usize,
data: []const u8,
};
const Result = struct {
task_id: usize,
hash: u64,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const num_workers = 4;
// Channels para distribuição e coleta
var task_channel = try std.Channel(Task).init(allocator, 100);
defer task_channel.deinit();
var result_channel = try std.Channel(Result).init(allocator, 100);
defer result_channel.deinit();
// Iniciar workers
var workers: [num_workers]std.Thread = undefined;
for (&workers) |*w| {
w.* = try std.Thread.spawn(
.{},
workerFn,
.{&task_channel, &result_channel},
);
}
// Enviar tasks (fan-out)
for (0..20) |i| {
const task = Task{
.id = i,
.data = "dados para processar",
};
try task_channel.send(task);
}
task_channel.close();
// Coletar resultados (fan-in)
var results_count: usize = 0;
while (result_channel.receive()) |result| {
std.debug.print("Resultado {d}: hash={d}\n",
.{result.task_id, result.hash});
results_count += 1;
}
// Aguardar workers
for (workers) |w| w.join();
std.debug.print("\nTotal processado: {d}\n", .{results_count});
}
fn workerFn(
tasks: *std.Channel(Task),
results: *std.Channel(Result),
) !void {
while (tasks.receive()) |task| {
// Simula processamento
const hash = @as(u64, task.id) * 31;
try results.send(.{
.task_id = task.id,
.hash = hash,
});
}
}
Thread Pools: Paralelismo Eficiente
Criar threads é caro. Thread pools reutilizam threads para melhor performance.
Thread Pool Simples
const ThreadPool = struct {
const Task = struct {
runFn: *const fn (*anyopaque) void,
context: *anyopaque,
};
allocator: std.mem.Allocator,
threads: []std.Thread,
queue: std.ArrayList(Task),
mutex: std.Thread.Mutex,
cond: std.Thread.Condition,
shutdown: bool,
fn init(allocator: std.mem.Allocator, num_threads: usize) !ThreadPool {
var pool = ThreadPool{
.allocator = allocator,
.threads = try allocator.alloc(std.Thread, num_threads),
.queue = std.ArrayList(Task).init(allocator),
.mutex = .{},
.cond = .{},
.shutdown = false,
};
for (pool.threads) |*t| {
t.* = try std.Thread.spawn(.{}, workerLoop, .{&pool});
}
return pool;
}
fn deinit(self: *ThreadPool) void {
self.mutex.lock();
self.shutdown = true;
self.cond.broadcast();
self.mutex.unlock();
for (self.threads) |t| t.join();
self.allocator.free(self.threads);
self.queue.deinit();
}
fn submit(self: *ThreadPool, comptime T: type, context: *T,
comptime func: fn (*T) void) !void {
const wrapper = struct {
fn run(ptr: *anyopaque) void {
func(@ptrCast(@alignCast(ptr)));
}
}.run;
self.mutex.lock();
defer self.mutex.unlock();
try self.queue.append(.{
.runFn = wrapper,
.context = context,
});
self.cond.signal();
}
fn workerLoop(self: *ThreadPool) void {
while (true) {
self.mutex.lock();
while (self.queue.items.len == 0 and !self.shutdown) {
self.cond.wait(&self.mutex);
}
if (self.shutdown and self.queue.items.len == 0) {
self.mutex.unlock();
break;
}
const task = self.queue.orderedRemove(0);
self.mutex.unlock();
task.runFn(task.context);
}
}
};
// Uso
const WorkItem = struct {
id: usize,
result: i32,
};
fn processItem(item: *WorkItem) void {
// Simula trabalho
std.time.sleep(10 * std.time.ns_per_ms);
item.result = @intCast(i32, item.id * item.id);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var pool = try ThreadPool.init(allocator, 4);
defer pool.deinit();
var items: [10]WorkItem = undefined;
for (&items, 0..) |*item, i| {
item.id = i;
try pool.submit(WorkItem, item, processItem);
}
// Aguardar (em produção, use um mecanismo de notificação)
std.time.sleep(500 * std.time.ns_per_ms);
for (items) |item| {
std.debug.print("Item {d}: resultado = {d}\n",
.{item.id, item.result});
}
}
Thread Pool da Standard Library
Zig 0.12+ inclui std.Thread.Pool:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Criar pool com 4 threads
var pool: std.Thread.Pool = undefined;
try pool.init(.{
.allocator = allocator,
.n_jobs = 4,
});
defer pool.deinit();
// Executar tarefas
var wg = std.Thread.WaitGroup{};
for (0..10) |i| {
pool.spawnWg(&wg, workerFn, .{i});
}
// Aguardar todas
wg.wait();
}
fn workerFn(id: usize) void {
std.debug.print("Worker {d} executando\n", .{id});
std.time.sleep(50 * std.time.ns_per_ms);
}
Padrões de Concorrência Práticos
1. Pipeline
Processamento em estágios conectados por channels:
fn pipelineExample() !void {
// Stage 1: Gerar números
// Stage 2: Filtrar pares
// Stage 3: Calcular quadrados
// Stage 4: Somar
}
2. Worker Pool com Retry
const RetryableTask = struct {
data: []const u8,
max_retries: u32,
current_retry: u32 = 0,
};
fn workerWithRetry(tasks: *std.Channel(RetryableTask)) void {
while (tasks.receive()) |*task| {
while (task.current_retry <= task.max_retries) : (task.current_retry += 1) {
if (tryProcess(task.data)) {
break; // Sucesso
}
std.time.sleep(std.time.ns_per_ms * 100 * task.current_retry);
}
}
}
fn tryProcess(data: []const u8) bool {
// Simula processamento que pode falhar
return std.crypto.random.boolean();
}
3. Parallel Map
fn parallelMap(
allocator: std.mem.Allocator,
input: []const i32,
comptime mapFn: fn (i32) i32,
) ![]i32 {
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = 4 });
defer pool.deinit();
var results = try allocator.alloc(i32, input.len);
errdefer allocator.free(results);
var wg = std.Thread.WaitGroup{};
for (input, 0..) |item, i| {
pool.spawnWg(&wg, struct {
fn run(ctx: struct {
item: i32,
out: *i32,
f: fn (i32) i32,
}) void {
ctx.out.* = ctx.f(ctx.item);
}
}.run, .{.{
.item = item,
.out = &results[i],
.f = mapFn,
}});
}
wg.wait();
return results;
}
fn double(x: i32) i32 { return x * 2; }
// Uso
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const input = &[_]i32{1, 2, 3, 4, 5};
const results = try parallelMap(allocator, input, double);
defer allocator.free(results);
std.debug.print("Resultados: {any}\n", .{results});
// [2, 4, 6, 8, 10]
}
Performance e Benchmarks
Comparativo de Overhead
| Operação | Latência aproximada | Quando usar |
|---|---|---|
| Spawn thread | ~10-100 μs | Longa duração |
| Mutex lock/unlock | ~20-50 ns | Seção crítica curta |
| Channel send (buffered) | ~50-100 ns | Comunicação thread-safe |
| Thread pool submit | ~200-500 ns | Tarefas frequentes |
| Context switch | ~1-10 μs | Evitar excesso |
Otimizações
- Evite contenção: Use múltiplos mutexes para diferentes dados
- Batch processing: Processe múltiplos itens por vez
- Lock-free quando possível: Use
std.atomicpara contadores simples - Alinhamento de cache: Estruture dados para evitar false sharing
Benchmark Simples
const std = @import("std");
fn benchmarkParallelSum(allocator: std.mem.Allocator, n: usize) !u64 {
var timer = try std.time.Timer.start();
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = 4 });
defer pool.deinit();
const chunk_size = n / 4;
var partial_sums: [4]u64 = [1]u64{0} ** 4;
var wg = std.Thread.WaitGroup{};
for (0..4) |i| {
pool.spawnWg(&wg, struct {
fn run(ctx: struct {
idx: usize,
start: usize,
end: usize,
out: *u64,
}) void {
var sum: u64 = 0;
for (ctx.start..ctx.end) |j| {
sum += j;
}
ctx.out.* = sum;
}
}.run, .{.{
.idx = i,
.start = i * chunk_size,
.end = if (i == 3) n else (i + 1) * chunk_size,
.out = &partial_sums[i],
}});
}
wg.wait();
var total: u64 = 0;
for (partial_sums) |s| total += s;
const elapsed = timer.read();
std.debug.print("Paralelo: {d} ns\n", .{elapsed});
return total;
}
fn benchmarkSequentialSum(n: usize) u64 {
var timer = std.time.Timer.start() catch unreachable;
var sum: u64 = 0;
for (0..n) |i| sum += i;
const elapsed = timer.read();
std.debug.print("Sequencial: {d} ns\n", .{elapsed});
return sum;
}
pub fn main() !void {
const n = 10_000_000;
std.debug.print("Somatório de 0 até {d}:\n\n", .{n});
const seq_result = benchmarkSequentialSum(n);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const par_result = try benchmarkParallelSum(gpa.allocator(), n);
std.debug.print("\nResultados iguais: {}\n", .{seq_result == par_result});
}
Armadilhas Comuns e Como Evitar
1. Data Race
// ❌ ERRADO: Data race
var counter: u64 = 0;
fn incrementUnsafe() void {
for (0..1000) |_| {
counter += 1; // Não é atômico!
}
}
// ✅ CORRETO: Use mutex
fn incrementSafe(mutex: *std.Thread.Mutex, counter: *u64) void {
for (0..1000) |_| {
mutex.lock();
defer mutex.unlock();
counter.* += 1;
}
}
2. Deadlock
// ❌ ERRADO: Ordem inconsistente de locks
fn transferAtoB(a: *Account, b: *Account, amount: u64) void {
a.mutex.lock();
b.mutex.lock(); // Pode deadlocked se outra thread faz B->A
// ...
}
// ✅ CORRETO: Ordem consistente
fn transferSafe(a: *Account, b: *Account, amount: u64) void {
const first = if (@intFromPtr(a) < @intFromPtr(b)) a else b;
const second = if (@intFromPtr(a) < @intFromPtr(b)) b else a;
first.mutex.lock();
defer first.mutex.unlock();
second.mutex.lock();
defer second.mutex.unlock();
// ...
}
3. Uso após Free
// ❌ ERRADO: Ponteiro para stack
fn badThread() !std.Thread {
var data: i32 = 42;
return std.Thread.spawn(.{}, worker, .{&data});
} // data é destruído aqui!
// ✅ CORRETO: Alocação no heap
fn goodThread(allocator: std.mem.Allocator) !std.Thread {
const data = try allocator.create(i32);
data.* = 42;
return std.Thread.spawn(.{}, workerWithFree, .{data, allocator});
}
fn workerWithFree(data: *i32, allocator: std.mem.Allocator) void {
std.debug.print("{d}\n", .{data.*});
allocator.destroy(data);
}
4. Busy Waiting
// ❌ ERRADO: Consome 100% CPU
while (!ready) {} // Busy wait
// ✅ CORRETO: Use condition variable
while (!ready) {
cond.wait(&mutex);
}
Exercícios Práticos
Exercício 1: Produtor/Consumidor
Implemente um sistema onde uma thread produz números primos e outra os imprime.
Ver solução
const std = @import("std");
fn isPrime(n: u32) bool {
if (n < 2) return false;
var i: u32 = 2;
while (i * i <= n) : (i += 1) {
if (n % i == 0) return false;
}
return true;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var channel = try std.Channel(u32).init(allocator, 10);
defer channel.deinit();
const producer = try std.Thread.spawn(.{}, struct {
fn run(ch: *std.Channel(u32)) !void {
var n: u32 = 2;
while (n < 100) : (n += 1) {
if (isPrime(n)) try ch.send(n);
}
}
}.run, .{&channel});
const consumer = try std.Thread.spawn(.{}, struct {
fn run(ch: *std.Channel(u32)) void {
while (ch.receive()) |prime| {
std.debug.print("Primo: {d}\n", .{prime});
}
}
}.run, .{&channel});
producer.join();
channel.close();
consumer.join();
}
Exercício 2: HTTP Download Paralelo
Baixe múltiplas URLs em paralelo usando um thread pool.
Ver solução
const std = @import("std");
const DownloadTask = struct {
url: []const u8,
content: []u8 = &.{},
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const urls = &[_][]const u8{
"https://example.com/1",
"https://example.com/2",
"https://example.com/3",
};
var tasks = try allocator.alloc(DownloadTask, urls.len);
defer allocator.free(tasks);
for (urls, 0..) |url, i| {
tasks[i] = .{ .url = url };
}
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = 4 });
defer pool.deinit();
var wg = std.Thread.WaitGroup{};
for (tasks) |*task| {
pool.spawnWg(&wg, downloadWorker, .{task, allocator});
}
wg.wait();
for (tasks) |task| {
std.debug.print("{s}: {d} bytes\n",
.{task.url, task.content.len});
allocator.free(task.content);
}
}
fn downloadWorker(task: *DownloadTask, allocator: std.mem.Allocator) void {
// Simula download
std.time.sleep(100 * std.time.ns_per_ms);
task.content = allocator.dupe(u8, "Conteúdo simulado")
catch &[_]u8{};
}
Exercício 3: Rate Limiter
Implemente um rate limiter que permite N requisições por segundo.
Ver solução
const std = @import("std");
const RateLimiter = struct {
mutex: std.Thread.Mutex,
tokens: u32,
last_update: i64,
max_tokens: u32,
refill_rate: u32, // tokens por segundo
fn init(max: u32, rate: u32) RateLimiter {
return .{
.mutex = .{},
.tokens = max,
.last_update = std.time.milliTimestamp(),
.max_tokens = max,
.refill_rate = rate,
};
}
fn tryAcquire(self: *RateLimiter) bool {
self.mutex.lock();
defer self.mutex.unlock();
const now = std.time.milliTimestamp();
const elapsed = @divTrunc(now - self.last_update, 1000);
const refill = @min(
self.max_tokens - self.tokens,
@as(u32, @intCast(elapsed)) * self.refill_rate,
);
self.tokens += refill;
self.last_update = now;
if (self.tokens > 0) {
self.tokens -= 1;
return true;
}
return false;
}
};
pub fn main() !void {
var limiter = RateLimiter.init(5, 2); // 5 burst, 2/segundo
for (0..10) |i| {
if (limiter.tryAcquire()) {
std.debug.print("Requisição {d}: permitida\n", .{i});
} else {
std.debug.print("Requisição {d}: rate limited\n", .{i});
}
std.time.sleep(200 * std.time.ns_per_ms);
}
}
FAQ
Preciso usar volatile para variáveis compartilhadas?
Não em Zig. Use std.atomic para operações atômicas ou mutex para seções críticas. volatile em Zig é apenas para MMIO (Memory-Mapped I/O).
Quantas threads devo criar?
- CPU-bound: Número de cores físicos
- I/O-bound: Pode ser maior (experimente 2x os cores)
- Mix: Profile e ajuste
Go tem goroutines, Rust tem tokio. Qual o equivalente Zig?
Zig não tem um runtime padrão. Você pode:
- Usar
std.Threadpara threads OS (mais simples) - Implementar green threads (avançado)
- Usar
xevouio_uringpara async - Usar bibliotecas de terceiros
Como fazer cancelamento de tarefas?
Use um canal de “done” ou std.atomic.Atomic(bool):
var should_stop = std.atomic.Atomic(bool).init(false);
// Na thread
while (!should_stop.load(.seq_cst)) {
// trabalho...
}
// Para cancelar
should_stop.store(true, .seq_cst);
Posso usar async/await com threads?
Sim! Veja nosso guia de async/await para detalhes. Em resumo:
const frame = async someAsyncFunction();
// ... outro trabalho ...
const result = await frame;
Threads são seguras em Zig?
Zig não tem borrow checker como Rust. Segurança é manual:
- Use
@compileErrorpara detectar erros em comptime - Use assertions (
std.debug.assert) para validar - Use
ThreadSanitizerem builds de debug
Próximos Passos
Agora que você domina concorrência em Zig:
- 🔧 Sistema de Build do Zig — Aprenda a configurar builds multi-thread
- ⚡ Async/Await em Zig — Para I/O-bound concurrency
- 🔒 Tratamento de Erros — Essencial para código concorrente robusto
- 🌐 Servidor HTTP — Aplica prática de concorrência
Recursos Adicionais
- Zig Language Reference — Concurrency
- Amdahl’s Law Calculator — Calcule speedup teórico
- ThreadSanitizer — Detecta data races
Gostou deste tutorial? Compartilhe com outros desenvolvedores e deixe suas dúvidas nos comentários!