Async/Await em Zig: Guia Prático de Programação Assíncrona

⚠️ Aviso Importante: O sistema de async/await em Zig está em evolução ativa. Enquanto os conceitos fundamentais são estáveis, algumas APIs específicas podem mudar entre versões. Este tutorial foca em padrões estabelecidos e adiciona notas onde há instabilidade.

Programação assíncrona é essencial para aplicações de alta performance que precisam lidar com milhares de conexões simultâneas. Zig oferece um modelo de async/await único — diferente de JavaScript, Rust ou Go — que dá ao desenvolvedor controle total sobre como o código assíncrono é executado.

Neste guia, você vai entender o modelo async de Zig desde os conceitos básicos até exemplos práticos de HTTP e manipulação de erros.

Por Que Zig para Async?

Antes de mergulharmos na sintaxe, vamos entender o que torna o modelo async do Zig especial:

CaracterísticaZigJavaScriptRustGo
RuntimeSem runtime obrigatórioV8 engineTokio/async-stdGoroutines
StackConfigurável (async/await = structs)Call stack + microtasksStackless coroutinesGrowable stacks
MemoryControle explícitoGC automáticoOwnershipGC
OverheadZero (sem runtime)Alto (event loop)MédioMédio
Color functionsNão (funções são agnósticas)Sim (async vs sync)SimNão

O Que Torna Zig Único

  1. Sem “function coloring”: Em Zig, funções async e sync têm a mesma assinatura
  2. Sem runtime obrigatório: Você escolhe (ou implementa) seu event loop
  3. Zero overhead: Async é implementado como state machines, não threads
  4. Composição elegante: async/await/suspend/resume funcionam em harmonia

Conceitos Fundamentais

O Que é um “Frame”?

Em Zig, quando você marca uma função como async, o compilador transforma ela em uma state machine (máquina de estados). Essa state machine é chamada de “frame” (quadro):

const std = @import("std");

// Esta função async pode ser suspensa e retomada
fn fetchData() ![]const u8 {
    // Simula uma operação de I/O
    std.time.sleep(100 * std.time.ns_per_ms);
    return "dados carregados";
}

pub fn main() !void {
    // Inicia a função async - retorna um frame, não o resultado
    var frame = async fetchData();
    
    // await retoma a execução e retorna o resultado
    const resultado = try await frame;
    
    std.debug.print("Resultado: {s}\n", .{resultado});
}

async, await, suspend, resume

Quatro palavras-chave controlam a execução assíncrona:

Palavra-chaveFunção
asyncInicia uma função async, retorna um frame
awaitEspera o frame completar, retorna o resultado
suspendPausa a execução, retorna controle ao chamador
resumeRetoma a execução de um frame suspenso
const std = @import("std");

fn coroutineExample() void {
    std.debug.print("Início\n", .{});
    
    // Suspende a execução
    suspend {}
    
    std.debug.print("Após primeiro resume\n", .{});
    
    suspend {}
    
    std.debug.print("Após segundo resume\n", .{});
}

pub fn main() void {
    var frame = async coroutineExample();
    
    std.debug.print("Frame criado\n", .{});
    
    resume frame;
    std.debug.print("Primeiro resume\n", .{});
    
    resume frame;
    std.debug.print("Segundo resume\n", .{});
}

Output:

Início
Frame criado
Após primeiro resume
Primeiro resume
Após segundo resume
Segundo resume

Async/Await na Prática

Exemplo Básico: Download de Arquivos

const std = @import("std");

const DownloadError = error{
    NetworkError,
    Timeout,
};

fn downloadFile(url: []const u8) DownloadError![]const u8 {
    std.debug.print("Iniciando download de {s}...\n", .{url});
    
    // Simula latência de rede
    std.time.sleep(500 * std.time.ns_per_ms);
    
    // Simula possível erro
    if (url.len == 0) {
        return DownloadError.NetworkError;
    }
    
    return "Conteúdo do arquivo";
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    
    // Inicia múltiplos downloads concorrentemente
    const frame1 = async downloadFile("https://exemplo.com/arquivo1.txt");
    const frame2 = async downloadFile("https://exemplo.com/arquivo2.txt");
    const frame3 = async downloadFile("https://exemplo.com/arquivo3.txt");
    
    std.debug.print("Downloads iniciados!\n", .{});
    
    // Aguarda todos completarem
    const result1 = try await frame1;
    const result2 = try await frame2;
    const result3 = try await frame3;
    
    std.debug.print("Arquivo 1: {s}\n", .{result1});
    std.debug.print("Arquivo 2: {s}\n", .{result2});
    std.debug.print("Arquivo 3: {s}\n", .{result3});
}

Paralelismo com async

Aqui está o ponto crucial: async não é paralelismo. Frames rodam em uma única thread até você explicitamente distribuí-las. Para entender as diferentes abordagens de paralelismo real com threads, veja o tutorial de concorrência em Zig:

const std = @import("std");

fn computeIntensive(n: u32) u32 {
    var sum: u32 = 0;
    var i: u32 = 0;
    while (i < n) : (i += 1) {
        sum += i;
    }
    return sum;
}

pub fn main() !void {
    // Isso NÃO roda em paralelo por padrão
    const frame1 = async computeIntensive(100_000_000);
    const frame2 = async computeIntensive(100_000_000);
    
    const result1 = await frame1;
    const result2 = await frame2;
    
    std.debug.print("Resultados: {} e {}\n", .{result1, result2});
}

Para verdadeiro paralelismo, combine com threads:

const std = @import("std");

fn worker(id: u32) void {
    std.debug.print("Worker {} iniciado\n", .{id});
    std.time.sleep(100 * std.time.ns_per_ms);
    std.debug.print("Worker {} completado\n", .{id});
}

pub fn main() !void {
    // Cria threads que executam async
    const t1 = try std.Thread.spawn(.{}, asyncWorker, .{1});
    const t2 = try std.Thread.spawn(.{}, asyncWorker, .{2});
    
    t1.join();
    t2.join();
}

fn asyncWorker(id: u32) void {
    var frame = async worker(id);
    await frame;
}

Event Loop: O Coração do Async

Zig não fornece um event loop padrão — você escolhe ou implementa o seu. Isso dá flexibilidade total, mas também significa mais responsabilidade.

Event Loop Simples

const std = @import("std");

const EventLoop = struct {
    frames: std.ArrayList(anyframe),
    allocator: std.mem.Allocator,
    
    pub fn init(allocator: std.mem.Allocator) EventLoop {
        return .{
            .frames = std.ArrayList(anyframe).init(allocator),
            .allocator = allocator,
        };
    }
    
    pub fn deinit(self: *EventLoop) void {
        self.frames.deinit();
    }
    
    pub fn spawn(self: *EventLoop, frame: anyframe) !void {
        try self.frames.append(frame);
    }
    
    pub fn run(self: *EventLoop) void {
        // Simplificação: em produção, usar seleção I/O (epoll, kqueue, IOCP)
        for (self.frames.items) |frame| {
            resume frame;
        }
        self.frames.clearRetainingCapacity();
    }
};

fn task(id: u32) void {
    std.debug.print("Task {} executando\n", .{id});
    suspend {}
    std.debug.print("Task {} completada\n", .{id});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    var loop = EventLoop.init(allocator);
    defer loop.deinit();
    
    // Agenda tarefas
    try loop.spawn(async task(1));
    try loop.spawn(async task(2));
    try loop.spawn(async task(3));
    
    // Executa o loop
    loop.run();
}

⚠️ Nota: Em código de produção, use bibliotecas como libxev ou implementações do std.event (quando estável).

HTTP Client Assíncrono

Vamos criar um exemplo prático de HTTP client não-bloqueante:

const std = @import("std");

const HttpError = error{
    ConnectionFailed,
    Timeout,
    InvalidResponse,
};

const HttpResponse = struct {
    status: u16,
    body: []const u8,
    
    pub fn deinit(self: HttpResponse, allocator: std.mem.Allocator) void {
        allocator.free(self.body);
    }
};

// Simula um cliente HTTP assíncrono
fn httpGet(allocator: std.mem.Allocator, url: []const u8) HttpError!HttpResponse {
    std.debug.print("[HTTP] GET {s}\n", .{url});
    
    // Em código real, aqui faria I/O não-bloqueante
    // suspend e resume quando dados disponíveis
    std.time.sleep(200 * std.time.ns_per_ms);
    
    // Simula resposta
    const body = try std.fmt.allocPrint(allocator, "Response from {s}", .{url});
    
    return HttpResponse{
        .status = 200,
        .body = body,
    };
}

// Faz múltiplas requisições concorrentes
fn fetchMultiple(allocator: std.mem.Allocator, urls: []const []const u8) !void {
    // Cria frames para cada URL
    var frames = try allocator.alloc(@Frame(httpGet), urls.len);
    defer allocator.free(frames);
    
    // Inicia todas as requisições
    for (urls, 0..) |url, i| {
        frames[i] = async httpGet(allocator, url);
    }
    
    // Aguarda todas completarem
    for (frames, urls) |*frame, url| {
        const response = await frame catch |err| {
            std.debug.print("Erro ao buscar {s}: {}\n", .{url, err});
            continue;
        };
        defer response.deinit(allocator);
        
        std.debug.print("✓ {s}: status {}, body: {s}\n", .{
            url, response.status, response.body,
        });
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    const urls = &.{
        "https://api.exemplo.com/users",
        "https://api.exemplo.com/posts",
        "https://api.exemplo.com/comments",
    };
    
    try fetchMultiple(allocator, urls);
}

Error Handling em Async

Erros em código async funcionam de forma elegante com error unions:

const std = @import("std");

const DatabaseError = error{
    ConnectionLost,
    QueryTimeout,
    RecordNotFound,
};

// Função que pode falhar
async fn fetchUser(id: u32) DatabaseError!User {
    if (id == 0) {
        return DatabaseError.RecordNotFound;
    }
    
    // Simula I/O
    std.time.sleep(50 * std.time.ns_per_ms);
    
    return User{
        .id = id,
        .name = "João",
    };
}

const User = struct {
    id: u32,
    name: []const u8,
};

pub fn main() !void {
    // Propagação de erros funciona normalmente
    const frame1 = async fetchUser(1);
    const user1 = try await frame1;
    std.debug.print("Usuário: {s}\n", .{user1.name});
    
    // Tratando erro
    const frame2 = async fetchUser(0);
    const user2 = await frame2 catch |err| {
        std.debug.print("Erro ao buscar usuário 0: {}\n", .{err});
        return;
    };
    
    std.debug.print("Usuário: {s}\n", .{user2.name});
}

Padrão: Timeout com async

const std = @import("std");

fn operationWithTimeout(
    comptime F: type,
    frame: F,
    timeout_ms: u64,
) !F.return_type {
    const start = std.time.milliTimestamp();
    
    while (true) {
        // Verifica se completou (simplificado)
        // Em código real, verificaria estado do frame
        
        if (std.time.milliTimestamp() - start > timeout_ms) {
            return error.Timeout;
        }
        
        std.time.sleep(1 * std.time.ns_per_ms);
    }
}

Performance e Considerações

Quando Usar Async

Use async quando…Evite async quando…
Muitas conexões I/O (10K+)Código puramente CPU-bound
Latência é críticaOverhead de state machine não justifica
Precisa de controle fino de concorrênciaSimplicidade é prioridade
Recursos são limitadosTime to market é crítico

Comparação de Performance

OperaçãoSíncronoAsyncThreads
Memória por conexão~0 bytes~100 bytes~1 MB
Context switchN/A~50 ns~1-10 µs
EscalabilidadeBaixaAltaMédia
ComplexidadeBaixaMédiaMédia

Benchmark Simples

const std = @import("std");

fn benchmark() void {
    const start = std.time.milliTimestamp();
    
    // Cria 10.000 frames
    var frames: [10000]@Frame(dummyTask) = undefined;
    for (&frames, 0..) |*frame, i| {
        frame.* = async dummyTask(i);
    }
    
    // Aguarda todos
    for (&frames) |*frame| {
        await frame;
    }
    
    const elapsed = std.time.milliTimestamp() - start;
    std.debug.print("10.000 tasks em {}ms\n", .{elapsed});
}

fn dummyTask(id: usize) void {
    _ = id;
    suspend {}
}

Estado Atual do Async em Zig

⚠️ Atenção: A implementação de async/await em Zig está sendo refinada. Algumas APIs mencionadas podem mudar.

O Que é Estável

  • ✅ Sintaxe async/await/suspend/resume
  • ✅ Semântica de frames
  • ✅ Integração com error handling
  • ✅ Composição com structs

O Que Está Evoluindo

  • 🔄 Event loop padrão (std.event)
  • 🔄 Integração com I/O assíncrono do SO
  • 🔄 APIs de networking assíncronas (veja o guia de networking e sockets em Zig para uso prático)
  • 🔄 Debugging de código async

Alternativas para Produção

Se você precisa de async estável hoje:

  1. libxev — Event loop de alta performance
  2. aio — Abstração de I/O assíncrono
  3. io_uring (Linux) — Via bindings C
// Exemplo com libxev (pseudo-código)
const xev = @import("xev");

var loop = xev.Loop.init(.{});
defer loop.deinit();

// Agenda operação de I/O
loop.run();

Padrões Comuns

1. Async Iterator

fn AsyncIterator(comptime T: type) type {
    return struct {
        nextFn: *const fn () anyerror!?T,
        
        pub fn next(self: @This()) !?T {
            return try self.nextFn();
        }
    };
}

2. Async Channel

fn Channel(comptime T: type) type {
    return struct {
        queue: std.ArrayList(T),
        mutex: std.Thread.Mutex,
        
        pub fn send(self: *@This(), value: T) !void {
            self.mutex.lock();
            defer self.mutex.unlock();
            try self.queue.append(value);
        }
        
        pub fn receive(self: *@This()) !?T {
            self.mutex.lock();
            defer self.mutex.unlock();
            return self.queue.popOrNull();
        }
    };
}

3. Semaphore Assíncrono

const Semaphore = struct {
    permits: usize,
    
    pub fn acquire(self: *Semaphore) void {
        while (self.permits == 0) {
            suspend {}
        }
        self.permits -= 1;
    }
    
    pub fn release(self: *Semaphore) void {
        self.permits += 1;
        // Resume waiters...
    }
};

Resumo

Você aprendeu:

  • Frames: Funções async como state machines
  • Sintaxe: async, await, suspend, resume
  • Event loops: Implementação e uso
  • HTTP assíncrono: Cliente não-bloqueante
  • Error handling: Erros funcionam naturalmente com async
  • Performance: Quando e por que usar async

Checklist

  • Entenda que async ≠ paralelismo
  • Escolha seu event loop (ou implemente)
  • Use suspend/resume para controle fino
  • Trate erros com error unions
  • Meça antes de otimizar
  • Fique atento a mudanças nas APIs

Próximos Passos

Continue seu aprendizado:


Recursos Adicionais

Última atualização: 09 de fevereiro de 2026
Versão do Zig: 0.13.0
Nota: APIs de async sujeitas a mudanças

Continue aprendendo Zig

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