Perguntas de Entrevista sobre Concorrência em Zig
Concorrência é um tema central em entrevistas para posições de backend, infraestrutura e programação de sistemas em geral. Zig oferece primitivos de concorrência de baixo nível que dão controle total ao programador, sem as abstrações ocultas de linguagens com runtime gerenciado. Entender esses mecanismos profundamente é essencial.
Fundamentos
Qual a diferença entre concorrência e paralelismo?
Concorrência é sobre estruturar um programa para lidar com múltiplas tarefas (potencialmente intercaladas). Paralelismo é sobre executar múltiplas tarefas simultaneamente em hardware multicore.
Zig suporta ambos: threads do OS para paralelismo real, e mecanismos de IO assíncrono para concorrência de IO sem necessariamente usar múltiplos cores.
Como Zig gerencia threads?
Zig usa threads do sistema operacional via std.Thread:
fn trabalho(id: usize) void {
std.debug.print("Thread {} executando\n", .{id});
}
pub fn main() !void {
var threads: [4]std.Thread = undefined;
for (&threads, 0..) |*t, i| {
t.* = try std.Thread.spawn(.{}, trabalho, .{i});
}
for (threads) |t| {
t.join();
}
}
Não há green threads ou fibers como em Go. Isso dá mais controle sobre scheduling e uso de recursos, mas requer gerenciamento manual de sincronização.
Explique as primitivas de sincronização disponíveis em Zig.
Mutex: Exclusão mútua para proteger dados compartilhados.
var mutex = std.Thread.Mutex{};
var contador: u64 = 0;
// Em cada thread:
mutex.lock();
defer mutex.unlock();
contador += 1;
Condition Variable: Para threads esperarem por uma condição.
Atomic Operations: Operações atômicas para sincronização lock-free via @atomicLoad, @atomicStore, @atomicRmw.
ResetEvent/Semaphore: Para sinalização entre threads.
Perguntas Intermediárias
O que é uma race condition e como prevenir em Zig?
Race condition ocorre quando o resultado de um programa depende da ordem de execução de threads, e essa ordem não é determinística. Em Zig:
Prevenção:
- Mutexes: Proteger dados compartilhados com locks
- Atomics: Para operações simples sem locks
- Imutabilidade: Dados
constnão precisam de sincronização - Thread-local storage: Cada thread tem sua cópia dos dados
- Arquitetura message-passing: Threads se comunicam via canais/filas
Zig em modo debug pode detectar data races via Thread Sanitizer quando compilado com -fsanitize=thread.
O que é um deadlock e como evitá-lo?
Deadlock ocorre quando duas ou mais threads esperam uma pela outra indefinidamente, cada uma segurando um recurso que a outra precisa.
Estratégias de prevenção:
- Ordenação de locks: Sempre adquirir múltiplos locks na mesma ordem global
- Try-lock com timeout: Tentar adquirir lock com limite de tempo
- Hierarquia de locks: Definir níveis de prioridade para diferentes locks
- Evitar locks aninhados: Simplificar a lógica para minimizar a necessidade de múltiplos locks
Explique operações atômicas e quando usá-las em vez de mutexes.
Operações atômicas são instruções que executam de forma indivisível no hardware:
var contador = std.atomic.Value(u64).init(0);
// Incremento atômico — sem lock necessário
_ = contador.fetchAdd(1, .seq_cst);
// Leitura atômica
const valor = contador.load(.seq_cst);
Use atomics quando: A operação é simples (incrementar contador, trocar flag), o overhead de mutex é inaceitável, ou para implementar estruturas lock-free.
Use mutexes quando: A seção crítica envolve múltiplas operações, a lógica é complexa, ou a legibilidade é prioridade.
O que são memory orderings e por que importam?
Memory orderings controlam como operações atômicas são reordenadas pelo compilador e CPU:
.seq_cst(sequentially consistent): O mais restritivo. Garante ordem global. Use quando não tem certeza..acquire: Garante que leituras após esta operação veem escritas que aconteceram antes do release correspondente..release: Garante que escritas antes desta operação são visíveis para threads que fazem acquire..monotonic(relaxed): Apenas garante atomicidade. Mais performático mas difícil de usar corretamente.
Cenários Práticos
Como implementar um pool de threads em Zig?
Um thread pool mantém N threads pré-criadas que processam tarefas de uma fila:
const ThreadPool = struct {
threads: []std.Thread,
queue: TaskQueue,
mutex: std.Thread.Mutex,
condition: std.Thread.Condition,
shutdown: bool,
fn worker(self: *ThreadPool) void {
while (true) {
self.mutex.lock();
while (self.queue.isEmpty() and !self.shutdown) {
self.condition.wait(&self.mutex);
}
if (self.shutdown) {
self.mutex.unlock();
return;
}
const task = self.queue.dequeue();
self.mutex.unlock();
task.execute();
}
}
};
Como Zig lida com concorrência de IO?
Para IO concorrente sem threads por conexão, Zig suporta:
std.io.poll: Multiplexação de IO com interface portável- io_uring (Linux): Acesso a io_uring para IO assíncrono de alta performance
- epoll/kqueue: Via abstração da biblioteca padrão
Isso é especialmente relevante para servidores de rede e backend.
Descreva um padrão producer-consumer em Zig.
fn producer(queue: *ThreadSafeQueue, mutex: *std.Thread.Mutex, cond: *std.Thread.Condition) void {
for (0..100) |i| {
mutex.lock();
queue.push(i);
cond.signal();
mutex.unlock();
}
}
fn consumer(queue: *ThreadSafeQueue, mutex: *std.Thread.Mutex, cond: *std.Thread.Condition) void {
while (true) {
mutex.lock();
while (queue.isEmpty()) {
cond.wait(mutex);
}
const item = queue.pop();
mutex.unlock();
processar(item);
}
}
Preparação
Concorrência é um tema vasto. Complemente com:
- Perguntas de performance — concorrência afeta diretamente performance
- Perguntas de networking — IO concorrente em servidores
- Perguntas de memória — alocação thread-safe
- Desafios de código — problemas práticos de concorrência
- Tutoriais e projetos práticos