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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento básico de Zig. Consulte a introdução ao Zig
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
Sempre join ou detach: Toda thread criada deve ser finalizada com
join()(espera) oudetach()(independente).Cuidado com dados compartilhados: Use operações atômicas ou mutex para proteger dados compartilhados.
Não crie threads demais: O número ideal geralmente é próximo ao número de cores da CPU. Use thread pools.
Stack size: Configure
.stack_sizeemstd.Thread.SpawnConfigse a thread precisar de mais stack.Evite data races: O Zig não previne data races automaticamente. Use sincronização adequada.
Receitas Relacionadas
- Como usar Mutex em Zig - Sincronização entre threads
- Como usar Thread Pool em Zig - Pool de threads reutilizáveis
- Como usar operações atômicas em Zig - Operações sem lock
- Como usar canais de comunicação em Zig - Comunicação entre threads