std.event em Zig — Referência e Exemplos

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:

  1. Threads — Para paralelismo real (std.Thread, std.Thread.Pool)
  2. I/O Polling — Via std.posix.poll ou std.io.poll
  3. Bibliotecas externas — Como libxev ou io_uring wrappers

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

Tutoriais e Receitas Relacionados

Continue aprendendo Zig

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