Event Loop e I/O Assíncrono em Zig
O Zig historicamente explorou um modelo de programação assíncrona baseado em async frames e um event loop integrado à biblioteca padrão. Na versão atual (0.13.x / 0.14.x), o suporte a async/await está temporariamente removido enquanto é redesenhado. Este documento descreve os mecanismos atuais para I/O não-bloqueante e concorrência baseada em eventos no Zig, além das abordagens recomendadas.
Visão Geral
const std = @import("std");
Estado Atual do Async no Zig
O Zig teve suporte a async/await/suspend/resume em versões anteriores (0.10 e anteriores), mas essa funcionalidade foi temporariamente removida para um redesign completo. As abordagens recomendadas atualmente são:
- Threads — Para paralelismo real (
std.Thread,std.Thread.Pool) - I/O Polling — Via
std.posix.polloustd.io.poll - Bibliotecas externas — Como
libxevouio_uringwrappers
Mecanismos Disponíveis
std.io.poll
// Polling de múltiplos file descriptors
pub fn poll(fds: []std.posix.pollfd, timeout: i32) !usize
std.posix (APIs de sistema)
// epoll (Linux)
pub fn epoll_create1(flags: u32) !i32
pub fn epoll_ctl(epfd: i32, op: u32, fd: i32, event: *epoll_event) !void
pub fn epoll_wait(epfd: i32, events: []epoll_event, timeout: i32) !usize
// Pipe para comunicação entre threads
pub fn pipe() ![2]i32
pub fn pipe2(flags: u32) ![2]i32
Exemplo 1: I/O Não-Bloqueante com Threads
A abordagem mais comum atualmente é usar threads para operações de I/O:
const std = @import("std");
const net = std.net;
const Contexto = struct {
stream: net.Stream,
id: usize,
};
fn handleCliente(ctx: Contexto) void {
defer ctx.stream.close();
var buf: [1024]u8 = undefined;
const reader = ctx.stream.reader();
const writer = ctx.stream.writer();
while (true) {
const n = reader.read(&buf) catch break;
if (n == 0) break;
writer.writeAll(buf[0..n]) catch break;
}
std.debug.print("Cliente {d} desconectou\n", .{ctx.id});
}
pub fn main() !void {
const addr = try net.Address.parseIp4("127.0.0.1", 9000);
var server = try addr.listen(.{ .reuse_address = true });
defer server.deinit();
std.debug.print("Servidor em :9000 (thread por cliente)\n", .{});
var id: usize = 0;
while (true) {
const conn = server.accept() catch continue;
id += 1;
std.debug.print("Novo cliente {d}\n", .{id});
// Uma thread por cliente
const thread = std.Thread.spawn(.{}, handleCliente, .{
Contexto{ .stream = conn.stream, .id = id },
}) catch continue;
thread.detach();
}
}
Exemplo 2: Timer e Eventos com Thread Pool
const std = @import("std");
const Evento = struct {
nome: []const u8,
delay_ms: u64,
callback: *const fn ([]const u8) void,
};
fn executarEvento(evento: Evento) void {
std.time.sleep(evento.delay_ms * std.time.ns_per_ms);
evento.callback(evento.nome);
}
fn onEvento(nome: []const u8) void {
const ts = std.time.milliTimestamp();
std.debug.print("[{d}] Evento: {s}\n", .{ ts, nome });
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = 4 });
defer pool.deinit();
const eventos = [_]Evento{
.{ .nome = "heartbeat", .delay_ms = 100, .callback = onEvento },
.{ .nome = "timeout_check", .delay_ms = 200, .callback = onEvento },
.{ .nome = "flush_logs", .delay_ms = 50, .callback = onEvento },
.{ .nome = "gc_cycle", .delay_ms = 150, .callback = onEvento },
.{ .nome = "metrics_push", .delay_ms = 75, .callback = onEvento },
};
const inicio = std.time.milliTimestamp();
for (eventos) |evento| {
pool.spawn(executarEvento, .{evento});
}
pool.waitForIdle();
const fim = std.time.milliTimestamp();
std.debug.print("\nTodos os eventos processados em {d}ms\n", .{fim - inicio});
}
Exemplo 3: Padrão Reactor Simples
const std = @import("std");
const Reactor = struct {
const Handler = *const fn (usize) void;
handlers: std.AutoHashMap(usize, Handler),
pendentes: std.ArrayList(usize),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Reactor {
return .{
.handlers = std.AutoHashMap(usize, Handler).init(allocator),
.pendentes = std.ArrayList(usize).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Reactor) void {
self.handlers.deinit();
self.pendentes.deinit();
}
pub fn registrar(self: *Reactor, id: usize, handler: Handler) !void {
try self.handlers.put(id, handler);
}
pub fn emitir(self: *Reactor, id: usize) !void {
try self.pendentes.append(id);
}
pub fn processar(self: *Reactor) void {
for (self.pendentes.items) |id| {
if (self.handlers.get(id)) |handler| {
handler(id);
}
}
self.pendentes.clearRetainingCapacity();
}
};
fn onConexao(id: usize) void {
std.debug.print(" -> Handler de conexão (id={d})\n", .{id});
}
fn onDados(id: usize) void {
std.debug.print(" -> Handler de dados (id={d})\n", .{id});
}
fn onDesconexao(id: usize) void {
std.debug.print(" -> Handler de desconexão (id={d})\n", .{id});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var reactor = Reactor.init(gpa.allocator());
defer reactor.deinit();
// Registra handlers
try reactor.registrar(1, onConexao);
try reactor.registrar(2, onDados);
try reactor.registrar(3, onDesconexao);
// Simula loop de eventos
const stdout = std.io.getStdOut().writer();
for (0..3) |ciclo| {
try stdout.print("Ciclo {d}:\n", .{ciclo});
// Simula eventos
try reactor.emitir(1);
try reactor.emitir(2);
if (ciclo == 2) try reactor.emitir(3);
reactor.processar();
}
}
Futuro do Async em Zig
O redesign do async no Zig tem como objetivos:
- Sem coloração de funções: Funções sync e async devem ser intercambiáveis
- Integração com I/O: O event loop será integrado ao sistema de I/O
- Zero-cost: Overhead zero quando async não é usado
- Cancelamento: Suporte robusto a cancelamento de operações
Enquanto o redesign não é finalizado, use std.Thread e std.Thread.Pool para concorrência.
Módulos Relacionados
- std.Thread — Threads nativas
- std.Thread.Pool — Pool de threads
- std.Thread.Mutex — Sincronização
- std.net — Networking
- std.io — Entrada e saída