---
title: "Concorrência Avançada em Zig: Padrões e Performance"
url: "https://ziglang.com.br/artigos/zig-concorrencia-padroes-avancados/"
markdown_url: "https://ziglang.com.br/artigos/zig-concorrencia-padroes-avancados.MD"
description: "Padrões avançados de concorrência em Zig: thread pools, produtor-consumidor, atomics e lock-free. Exemplos práticos e benchmarks em português."
date: "2026-04-10"
author: ""
---

# Concorrência Avançada em Zig: Padrões e Performance

Padrões avançados de concorrência em Zig: thread pools, produtor-consumidor, atomics e lock-free. Exemplos práticos e benchmarks em português.


Se você já conhece o [básico de threads e concorrência em Zig](/tutoriais/concorrencia-em-zig/), está na hora de avançar. Neste artigo, vamos explorar **padrões de concorrência do mundo real** — thread pools, filas produtor-consumidor, operações atômicas e técnicas lock-free que fazem a diferença em aplicações de alta performance.

A abordagem do Zig para concorrência é fundamentalmente diferente de linguagens como Go ou Java. Não existe runtime gerenciando goroutines ou thread pools implícitas. Você constrói exatamente o que precisa, com controle total sobre cada byte de memória e cada ciclo de CPU.

## Thread Pool: O Padrão Mais Importante

Criar uma thread por tarefa (como fizemos no [artigo de networking](/artigos/zig-networking-sockets-tcp-udp/)) funciona para poucos clientes, mas não escala. Um thread pool reutiliza um número fixo de threads para processar muitas tarefas:

```zig
const std = @import("std");

pub fn ThreadPool(comptime Task: type) type {
    return struct {
        const Self = @This();

        threads: []std.Thread,
        queue: TaskQueue,
        mutex: std.Thread.Mutex = .{},
        condition: std.Thread.Condition = .{},
        shutdown: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
        allocator: std.mem.Allocator,

        const TaskQueue = std.DoublyLinkedList(Task);

        pub fn init(allocator: std.mem.Allocator, num_threads: usize) !Self {
            var pool = Self{
                .threads = try allocator.alloc(std.Thread, num_threads),
                .queue = .{},
                .allocator = allocator,
            };

            // Inicia as worker threads
            for (pool.threads) |*t| {
                t.* = try std.Thread.spawn(.{}, workerLoop, .{&pool});
            }

            return pool;
        }

        pub fn submit(self: *Self, task: Task) !void {
            const node = try self.allocator.create(TaskQueue.Node);
            node.* = .{ .data = task };

            self.mutex.lock();
            self.queue.append(node);
            self.mutex.unlock();

            self.condition.signal();
        }

        fn workerLoop(pool: *Self) void {
            while (!pool.shutdown.load(.acquire)) {
                pool.mutex.lock();

                // Espera até ter uma tarefa ou shutdown
                while (pool.queue.len == 0 and !pool.shutdown.load(.acquire)) {
                    pool.condition.wait(&pool.mutex);
                }

                const node = pool.queue.popFirst();
                pool.mutex.unlock();

                if (node) |n| {
                    // Executa a tarefa
                    n.data.execute();
                    pool.allocator.destroy(n);
                }
            }
        }

        pub fn deinit(self: *Self) void {
            self.shutdown.store(true, .release);
            self.condition.broadcast();

            for (self.threads) |t| {
                t.join();
            }
            self.allocator.free(self.threads);
        }
    };
}
```

O uso de [comptime](/glossario/comptime/) para parametrizar o tipo `Task` é um padrão idiomático do Zig — o compilador gera código especializado para cada tipo de tarefa, sem overhead de vtables ou dispatch dinâmico. Isso é uma vantagem significativa sobre thread pools genéricos em C++ ou Java que dependem de herança ou interfaces.

### Usando o Thread Pool

```zig
const MyTask = struct {
    id: u32,
    data: []const u8,

    pub fn execute(self: *const MyTask) void {
        std.debug.print("Thread {any} processando tarefa {d}: {s}\n", .{
            std.Thread.getCurrentId(),
            self.id,
            self.data,
        });
        // Simula trabalho pesado
        std.time.sleep(100 * std.time.ns_per_ms);
    }
};

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

    var pool = try ThreadPool(MyTask).init(gpa.allocator(), 4);
    defer pool.deinit();

    // Submete 20 tarefas para 4 threads
    for (0..20) |i| {
        try pool.submit(.{
            .id = @intCast(i),
            .data = "processamento batch",
        });
    }
}
```

## Padrão Produtor-Consumidor com Fila Thread-Safe

O produtor-consumidor é fundamental para pipelines de processamento. Vamos implementar uma fila bloqueante que permite múltiplos produtores e consumidores:

```zig
const std = @import("std");

pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
    return struct {
        const Self = @This();

        buffer: [capacity]T = undefined,
        head: usize = 0,
        tail: usize = 0,
        count: usize = 0,
        mutex: std.Thread.Mutex = .{},
        not_empty: std.Thread.Condition = .{},
        not_full: std.Thread.Condition = .{},

        pub fn push(self: *Self, item: T) void {
            self.mutex.lock();
            defer self.mutex.unlock();

            // Espera até ter espaço
            while (self.count == capacity) {
                self.not_full.wait(&self.mutex);
            }

            self.buffer[self.tail] = item;
            self.tail = (self.tail + 1) % capacity;
            self.count += 1;

            self.not_empty.signal();
        }

        pub fn pop(self: *Self) T {
            self.mutex.lock();
            defer self.mutex.unlock();

            // Espera até ter um item
            while (self.count == 0) {
                self.not_empty.wait(&self.mutex);
            }

            const item = self.buffer[self.head];
            self.head = (self.head + 1) % capacity;
            self.count -= 1;

            self.not_full.signal();
            return item;
        }
    };
}
```

Essa fila circular com tamanho fixo é alocada inteiramente na stack (ou dentro da struct que a contém), sem nenhuma chamada de [allocator](/glossario/allocator/). Isso a torna extremamente rápida e previsível — ideal para sistemas [embarcados](/artigos/zig-embarcados-iot/) ou de [tempo real](/artigos/zig-real-time-systems/).

## Operações Atômicas e Lock-Free

Para cenários onde mutexes são caros demais, Zig oferece `std.atomic` com suporte a todas as ordens de memória. Vamos implementar um contador atômico e uma flag de spin-lock:

```zig
const std = @import("std");

const AtomicCounter = struct {
    value: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),

    pub fn increment(self: *AtomicCounter) u64 {
        return self.value.fetchAdd(1, .monotonic);
    }

    pub fn get(self: *const AtomicCounter) u64 {
        return self.value.load(.acquire);
    }
};

// SpinLock — útil para seções críticas muito curtas
const SpinLock = struct {
    locked: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),

    pub fn acquire(self: *SpinLock) void {
        while (self.locked.cmpxchgWeak(
            false,
            true,
            .acquire,
            .monotonic,
        ) != null) {
            // Spin hint — reduz consumo de energia no loop
            std.atomic.spinLoopHint();
        }
    }

    pub fn release(self: *SpinLock) void {
        self.locked.store(false, .release);
    }
};
```

### Quando Usar Cada Abordagem

| Padrão | Latência | Uso de CPU | Melhor Para |
|--------|----------|------------|-------------|
| Mutex + Condition | ~100ns | Baixo | Esperas longas, I/O |
| SpinLock | ~10ns | Alto | Seções críticas < 1μs |
| Atomics | ~5ns | Mínimo | Contadores, flags |
| Lock-free queue | ~20ns | Médio | Alta concorrência |

A regra geral: comece com mutex (é mais simples e correto), e só migre para atomics ou lock-free quando o profiling mostrar que o lock é o gargalo. Zig facilita esse [benchmarking](/receitas/zig-benchmark-performance/) com ferramentas nativas.

## WaitGroup: Sincronização de Tarefas

Para esperar que múltiplas tarefas concluam, o padrão WaitGroup é essencial — similar ao `sync.WaitGroup` de <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a>:

```zig
const std = @import("std");

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

    const num_workers = 8;
    var wait_group = std.Thread.WaitGroup{};

    var threads = try allocator.alloc(std.Thread, num_workers);
    defer allocator.free(threads);

    // Inicia workers
    for (threads, 0..) |*t, i| {
        wait_group.start();
        t.* = try std.Thread.spawn(.{}, worker, .{ &wait_group, i });
    }

    // Espera todos terminarem
    wait_group.wait();
    std.debug.print("Todas as {d} tarefas concluídas!\n", .{num_workers});
}

fn worker(wg: *std.Thread.WaitGroup, id: usize) void {
    defer wg.finish();

    std.debug.print("Worker {d} iniciando...\n", .{id});
    // Simula trabalho
    std.time.sleep(std.time.ns_per_s * (id % 3 + 1));
    std.debug.print("Worker {d} concluído.\n", .{id});
}
```

## Padrão Fan-Out/Fan-In para Processamento Paralelo

Um dos padrões mais úteis para processamento de dados em lote é o fan-out/fan-in — distribuir trabalho entre múltiplas threads e coletar os resultados:

```zig
const std = @import("std");

fn parallelMap(
    comptime T: type,
    comptime R: type,
    items: []const T,
    results: []R,
    comptime mapFn: fn (T) R,
    num_threads: usize,
) !void {
    const chunk_size = (items.len + num_threads - 1) / num_threads;
    var wait_group = std.Thread.WaitGroup{};

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var threads = try gpa.allocator().alloc(std.Thread, num_threads);
    defer gpa.allocator().free(threads);

    var spawned: usize = 0;
    for (0..num_threads) |i| {
        const start = i * chunk_size;
        if (start >= items.len) break;
        const end = @min(start + chunk_size, items.len);

        wait_group.start();
        threads[i] = try std.Thread.spawn(.{}, struct {
            fn run(wg: *std.Thread.WaitGroup, src: []const T, dst: []R) void {
                defer wg.finish();
                for (src, 0..) |item, j| {
                    dst[j] = mapFn(item);
                }
            }
        }.run, .{ &wait_group, items[start..end], results[start..end] });
        spawned += 1;
    }

    wait_group.wait();
}
```

Este padrão é especialmente poderoso combinado com [SIMD](/artigos/zig-simd-processamento-vetorial/) para processamento vetorial — cada thread pode processar um chunk dos dados usando instruções SIMD, multiplicando a performance.

## Comparação com Outras Linguagens

### Zig vs Go

<a href="https://golang.com.br/aprenda/concorrencia-go/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> oferece goroutines e channels como primitivas de primeira classe, com um scheduler M:N sofisticado. A vantagem é a facilidade de uso — `go func()` e pronto. A desvantagem é o overhead do runtime (~4KB por goroutine, scheduler, GC) e a falta de controle sobre afinidade de thread, prioridades e alocação de memória.

Em Zig, você monta a infraestrutura que precisa, nem mais nem menos. É mais trabalho inicial, mas o resultado é código que roda em microsegundos, não milissegundos.

### Zig vs Rust

<a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a> garante ausência de data races em tempo de compilação com ownership e o trait `Send/Sync`. É poderoso, mas adiciona complexidade — lifetimes em estruturas concorrentes podem ser desafiadores. Zig não tem essas garantias estáticas, mas compensa com um runtime de debug que detecta data races em tempo de execução (via Thread Sanitizer).

### Zig vs Kotlin/JVM

Se você vem de <a href="https://kotlin.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'kotlin.dev.br' })">Kotlin</a> e suas coroutines, a diferença fundamental é que Zig não tem GC. Em Kotlin, você pode criar milhões de coroutines porque a JVM gerencia a memória. Em Zig, cada thread tem um custo real de stack (~8MB padrão no Linux), então thread pools são essenciais.

## Armadilhas Comuns

1. **Data races**: Zig não previne data races em compilação — use `-fsanitize=thread` no [modo de debug](/glossario/release-modes/) para detectá-las
2. **Deadlocks**: sempre adquira locks na mesma ordem global; considere usar `tryLock` com timeout
3. **False sharing**: alinhe dados compartilhados em cache lines (64 bytes) com `align(64)`
4. **Stack overflow em threads**: o tamanho padrão da stack pode não ser suficiente — configure via `std.Thread.SpawnConfig`
5. **Esquecimento de [defer](/glossario/defer/)**: sempre use `defer mutex.unlock()` imediatamente após o lock

## Conclusão

Concorrência em Zig não é para iniciantes — exige entender threads, memória e sincronização em nível de sistema. Mas essa explicitação é justamente o ponto: você sabe exatamente o que seu programa faz, sem mágica de runtime.

Para começar, construa um thread pool simples e evolua conforme a necessidade. Se você quer ver esses padrões aplicados na prática, confira nosso artigo sobre [networking com sockets](/artigos/zig-networking-sockets-tcp-udp/) que usa threads para atender múltiplos clientes.

### Leitura Complementar

- [Concorrência básica em Zig](/tutoriais/concorrencia-em-zig/) — fundamentos de threads e channels
- [io_uring e async I/O](/artigos/zig-io-uring-async/) — I/O assíncrono de alta performance no Linux
- [SIMD e processamento vetorial](/artigos/zig-simd-processamento-vetorial/) — combine paralelismo de thread com paralelismo de dados
- [Testes em Zig](/artigos/zig-testes-guia-completo/) — como testar código concorrente
- [Error handling](/artigos/zig-error-handling-boas-praticas/) — tratamento de erros em contextos multi-thread
- [Clean code em Zig](/artigos/zig-clean-code/) — boas práticas para código legível

---

*Explore nosso [glossário](/glossario/) para entender termos como [allocator](/glossario/allocator/), [comptime](/glossario/comptime/) e [error union](/glossario/error-union/), ou mergulhe nos [tutoriais](/tutoriais/) para aprender Zig do zero.*
