Como Criar e Gerenciar Threads em Zig

Introdução

Threads permitem executar múltiplas tarefas simultaneamente, aproveitando os múltiplos núcleos do processador. Em Zig, a API std.Thread oferece uma interface direta para criar, gerenciar e sincronizar threads de forma segura.

Nesta receita, você aprenderá a criar threads, passar dados entre elas e sincronizar resultados.

Pré-requisitos

Criar uma Thread Simples

O exemplo mais básico: criar uma thread que executa uma função:

const std = @import("std");

fn tarefa() void {
    std.debug.print("Olá da thread!\n", .{});
}

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

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

    std.debug.print("Thread concluída. Main continua.\n", .{});
}

Saída esperada

Olá da thread!
Thread concluída. Main continua.

Passar Argumentos para Threads

Passe dados para a função da thread:

const std = @import("std");

fn calcularSoma(inicio: u64, fim: u64) u64 {
    var soma: u64 = 0;
    var i = inicio;
    while (i <= fim) : (i += 1) {
        soma += i;
    }
    std.debug.print("Soma de {d} a {d} = {d}\n", .{ inicio, fim, soma });
    return soma;
}

fn threadCalculo(inicio: u64, fim: u64) void {
    _ = calcularSoma(inicio, fim);
}

pub fn main() !void {
    // Criar múltiplas threads com argumentos diferentes
    const t1 = try std.Thread.spawn(.{}, threadCalculo, .{ 1, 1000 });
    const t2 = try std.Thread.spawn(.{}, threadCalculo, .{ 1001, 2000 });
    const t3 = try std.Thread.spawn(.{}, threadCalculo, .{ 2001, 3000 });

    // Esperar todas terminarem
    t1.join();
    t2.join();
    t3.join();

    std.debug.print("Todos os cálculos concluídos.\n", .{});
}

Threads com Dados Compartilhados

Use ponteiros para compartilhar dados entre threads (com cuidado):

const std = @import("std");

const Contador = struct {
    valor: std.atomic.Value(u64),

    pub fn init() Contador {
        return .{
            .valor = std.atomic.Value(u64).init(0),
        };
    }

    pub fn incrementar(self: *Contador) void {
        _ = self.valor.fetchAdd(1, .seq_cst);
    }

    pub fn get(self: *Contador) u64 {
        return self.valor.load(.seq_cst);
    }
};

fn workerThread(contador: *Contador, n: u32) void {
    for (0..n) |_| {
        contador.incrementar();
    }
}

pub fn main() !void {
    var contador = Contador.init();

    const num_threads = 4;
    const incrementos_por_thread = 10000;

    var threads: [num_threads]std.Thread = undefined;

    // Iniciar threads
    for (&threads) |*t| {
        t.* = try std.Thread.spawn(.{}, workerThread, .{
            &contador,
            incrementos_por_thread,
        });
    }

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

    const total = contador.get();
    const esperado = num_threads * incrementos_por_thread;
    std.debug.print("Total: {d} (esperado: {d})\n", .{ total, esperado });
    std.debug.print("Correto: {}\n", .{total == esperado});
}

Saída esperada

Total: 40000 (esperado: 40000)
Correto: true

Criar Múltiplas Threads Dinamicamente

Use um allocator para criar um número variável de threads:

const std = @import("std");

fn processarItem(id: usize) void {
    // Simular trabalho
    std.time.sleep(std.time.ns_per_ms * 100);
    std.debug.print("Item {d} processado pela thread\n", .{id});
}

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

    const num_itens: usize = 8;

    // Alocar array de threads
    const threads = try allocator.alloc(std.Thread, num_itens);
    defer allocator.free(threads);

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

    std.debug.print("Todas as {d} threads iniciadas. Aguardando...\n", .{num_itens});

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

    std.debug.print("Todos os itens processados!\n", .{});
}

Thread com Detach

Para threads que não precisam ser aguardadas:

const std = @import("std");

fn backgroundTask() void {
    std.debug.print("Tarefa em background iniciada.\n", .{});
    std.time.sleep(std.time.ns_per_s * 1);
    std.debug.print("Tarefa em background concluída.\n", .{});
}

pub fn main() !void {
    const thread = try std.Thread.spawn(.{}, backgroundTask, .{});

    // Detach: thread continua independente
    thread.detach();

    std.debug.print("Main continua sem esperar a thread.\n", .{});

    // Precisamos esperar um pouco para a thread terminar antes do processo sair
    std.time.sleep(std.time.ns_per_s * 2);
}

Padrão Produtor-Consumidor Simples

Threads coordenadas com dados atômicos:

const std = @import("std");

const FilaSimples = struct {
    itens: [256]u32 = undefined,
    head: std.atomic.Value(usize),
    tail: std.atomic.Value(usize),

    pub fn init() FilaSimples {
        return .{
            .head = std.atomic.Value(usize).init(0),
            .tail = std.atomic.Value(usize).init(0),
        };
    }

    pub fn push(self: *FilaSimples, valor: u32) bool {
        const tail = self.tail.load(.seq_cst);
        const next_tail = (tail + 1) % 256;
        if (next_tail == self.head.load(.seq_cst)) return false; // cheio
        self.itens[tail] = valor;
        self.tail.store(next_tail, .seq_cst);
        return true;
    }

    pub fn pop(self: *FilaSimples) ?u32 {
        const head = self.head.load(.seq_cst);
        if (head == self.tail.load(.seq_cst)) return null; // vazio
        const valor = self.itens[head];
        self.head.store((head + 1) % 256, .seq_cst);
        return valor;
    }
};

fn produtor(fila: *FilaSimples) void {
    for (0..20) |i| {
        while (!fila.push(@intCast(i))) {
            std.time.sleep(std.time.ns_per_ms);
        }
        std.debug.print("Produzido: {d}\n", .{i});
    }
}

fn consumidor(fila: *FilaSimples) void {
    var consumidos: u32 = 0;
    while (consumidos < 20) {
        if (fila.pop()) |valor| {
            std.debug.print("  Consumido: {d}\n", .{valor});
            consumidos += 1;
        } else {
            std.time.sleep(std.time.ns_per_ms);
        }
    }
}

pub fn main() !void {
    var fila = FilaSimples.init();

    const t_prod = try std.Thread.spawn(.{}, produtor, .{&fila});
    const t_cons = try std.Thread.spawn(.{}, consumidor, .{&fila});

    t_prod.join();
    t_cons.join();

    std.debug.print("Produtor-consumidor concluído!\n", .{});
}

Dicas e Boas Práticas

  1. Sempre join ou detach: Toda thread criada deve ser finalizada com join() (espera) ou detach() (independente).

  2. Cuidado com dados compartilhados: Use operações atômicas ou mutex para proteger dados compartilhados.

  3. Não crie threads demais: O número ideal geralmente é próximo ao número de cores da CPU. Use thread pools.

  4. Stack size: Configure .stack_size em std.Thread.SpawnConfig se a thread precisar de mais stack.

  5. Evite data races: O Zig não previne data races automaticamente. Use sincronização adequada.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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