Concorrência em Zig: Threads, Channels e Padrões de Paralelismo

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/await do 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

  1. Modelo de Concorrência do Zig
  2. Threads em Zig: O Básico
  3. Sincronização: Mutex e Condition
  4. Channels: Comunicação Entre Threads
  5. Thread Pools: Paralelismo Eficiente
  6. Padrões de Concorrência Práticos
  7. Performance e Benchmarks
  8. Armadilhas Comuns e Como Evitar
  9. Exercícios Práticos
  10. FAQ
  11. 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

AspectoZigGoRustC++
ModeloThreads explícitas + Async opt-inGoroutines (green threads)Ownership + borrowingstd::thread + futures
OverheadZero (OS threads)Baixo (~2KB/goroutine)ZeroZero
ControleTotalAltoAltoAlto
SegurançaManual (com asserts)GC garanteCompilador garanteManual
Curva de aprendizadoMédiaBaixaAltaAlta

Princípios do Zig para Concorrência

  1. Sem runtime: Zig não tem scheduler embutido — você controla tudo
  2. Zero overhead: Sem garbage collector, sem green threads
  3. Composição explícita: Você escolhe o nível de abstração
  4. Erros tratáveis: Falhas de alocação são try/catch, não panic

Quando Usar Cada Abordagem

CenárioAbordagem RecomendadaPor quê?
I/O-bound (rede, disco)Async/awaitMenos threads, mais conexões
CPU-bound (cálculos)Thread poolUsa todos os cores
Coordenação simplesChannelsPadrão produtor/consumidor claro
Estado compartilhadoMutex + condControle fino de acesso
High-performance serverI/O uring + asyncMá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çãoLatência aproximadaQuando usar
Spawn thread~10-100 μsLonga duração
Mutex lock/unlock~20-50 nsSeção crítica curta
Channel send (buffered)~50-100 nsComunicação thread-safe
Thread pool submit~200-500 nsTarefas frequentes
Context switch~1-10 μsEvitar excesso

Otimizações

  1. Evite contenção: Use múltiplos mutexes para diferentes dados
  2. Batch processing: Processe múltiplos itens por vez
  3. Lock-free quando possível: Use std.atomic para contadores simples
  4. 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:

  1. Usar std.Thread para threads OS (mais simples)
  2. Implementar green threads (avançado)
  3. Usar xev ou io_uring para async
  4. 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 @compileError para detectar erros em comptime
  • Use assertions (std.debug.assert) para validar
  • Use ThreadSanitizer em builds de debug

Próximos Passos

Agora que você domina concorrência em Zig:

  1. 🔧 Sistema de Build do Zig — Aprenda a configurar builds multi-thread
  2. Async/Await em Zig — Para I/O-bound concurrency
  3. 🔒 Tratamento de Erros — Essencial para código concorrente robusto
  4. 🌐 Servidor HTTP — Aplica prática de concorrência

Recursos Adicionais


Gostou deste tutorial? Compartilhe com outros desenvolvedores e deixe suas dúvidas nos comentários!

Continue aprendendo Zig

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