Zig Async/Await: O Novo Sistema std.Io Explicado

Zig Async/Await: O Novo Sistema std.Io Explicado

O sistema de I/O assíncrono do Zig passou por uma reformulação completa. O antigo modelo de async/await — que havia sido temporariamente removido — voltou repensado do zero com o std.Io, uma interface que separa a expressão de concorrência do modelo de execução. O resultado? Código que funciona de forma ótima tanto em modo síncrono quanto assíncrono, sem precisar de duas versões da mesma biblioteca.

Neste artigo, vamos explorar como o novo std.Io funciona, por que ele é diferente de qualquer outro modelo async que você já viu, e como usá-lo na prática.

Se você ainda não conhece Zig, comece por O que é Zig ou veja Por que aprender Zig.

O Problema que o std.Io Resolve

Em linguagens como JavaScript, Rust e Go, o modelo de concorrência é definido pela linguagem. Em JS, tudo é single-threaded com event loop. Em Go, tudo são goroutines. Em Rust, você escolhe um runtime (tokio, async-std), mas o código é “colorido” — funções async e sync são incompatíveis.

Zig tomou uma decisão radical: o modelo de concorrência é injetado, não fixo. Assim como o allocator é passado como parâmetro em Zig (em vez de usar malloc global), o Io também é passado como parâmetro. Isso significa que a mesma função pode rodar:

  • Bloqueante — I/O síncrono direto, sem overhead
  • Thread pool — multiplexando chamadas em threads do SO
  • Green threads — usando io_uring no Linux
  • Coroutines stackless — máquinas de estado, compatível com WebAssembly

Anatomia do std.Io

O std.Io é uma interface que encapsula todas as operações de I/O: sistema de arquivos, rede, timers e sincronização. Veja a estrutura básica:

const std = @import("std");
const Io = std.Io;

pub fn main(io: Io) !void {
    const file = try Io.Dir.cwd().createFile(io, "dados.txt", .{});
    defer file.close(io);
    try file.writeAll(io, "Olá do Zig assíncrono!\n");
}

Repare que io é passado como parâmetro — exatamente como fazemos com allocators. Não há nenhuma keyword async na assinatura da função. O código é agnóstico ao modelo de concorrência.

Comparação com o Modelo Anterior

No antigo sistema (removido na versão 0.11), async/await eram keywords da linguagem com semântica fixa:

// ANTIGO — não funciona mais
const frame = async readFile("dados.txt");
const result = await frame;

O novo sistema é fundamentalmente diferente — a concorrência é expressa via a API std.Io, não via keywords do compilador.

Futures e Concorrência

A grande novidade é o modelo de futures. Quando você quer executar operações em paralelo, usa io.async():

const std = @import("std");
const Io = std.Io;

fn salvarBackup(io: Io, dados: []const u8) !void {
    // Inicia duas operações em paralelo
    var future_local = io.async(salvarArquivo, .{ io, dados, "backup_local.dat" });
    var future_remoto = io.async(salvarArquivo, .{ io, dados, "backup_remoto.dat" });

    // Aguarda ambas completarem
    try future_local.await(io);
    try future_remoto.await(io);
}

fn salvarArquivo(io: Io, dados: []const u8, caminho: []const u8) !void {
    const file = try Io.Dir.cwd().createFile(io, caminho, .{});
    defer file.close(io);
    try file.writeAll(io, dados);
}

Esse código expressa que as duas operações de salvamento podem rodar em paralelo. Se o runtime fornecido for bloqueante, elas rodam sequencialmente. Se for green threads com io_uring, rodam de verdade em paralelo. O código não muda.

Cancelamento de Futures

Toda future suporta cancelamento, o que é essencial para operações com timeout ou para liberar recursos:

fn buscarComTimeout(io: Io, url: []const u8) ![]const u8 {
    var future = io.async(fazerRequisicao, .{ io, url });
    defer future.cancel(io) catch {};

    // Se demorar demais, o defer cancela automaticamente
    const resultado = try future.await(io);
    return resultado;
}

O padrão defer future.cancel() garante que a operação é cancelada se a função sair por qualquer motivo — erro, retorno antecipado ou errdefer.

Modelos de Execução

A beleza do std.Io é que você escolhe o modelo na hora de rodar, não na hora de escrever. Veja os quatro modelos disponíveis:

1. Blocking I/O

O mais simples. Cada chamada de I/O bloqueia a thread atual:

pub fn main() !void {
    var io = std.Io.blocking();
    try meuServidor(io);
}

Perfeito para CLIs, scripts e ferramentas de build. Zero overhead, zero alocações extras.

2. Thread Pool

Multiplica operações bloqueantes em threads do SO:

pub fn main() !void {
    var io = std.Io.threadPool(.{ .thread_count = 8 });
    defer io.deinit();
    try meuServidor(io);
}

Bom para servidores com carga moderada. Cada future pode rodar em uma thread diferente.

3. Green Threads (io_uring)

No Linux, usa io_uring para I/O assíncrono de verdade com green threads leves:

pub fn main() !void {
    var io = std.Io.greenThreads(.{ .entries = 256 });
    defer io.deinit();
    try meuServidor(io);
}

Essa é a opção mais performática para servidores de alta carga. Milhares de conexões simultâneas com uso mínimo de memória — semelhante ao que o TigerBeetle faz em produção.

4. Stackless Coroutines

Transforma futures em máquinas de estado, compatível com ambientes sem stack como WebAssembly:

pub fn main() !void {
    var io = std.Io.stackless();
    defer io.deinit();
    try meuServidor(io);
}

Ideal para aplicações WASM e sistemas embarcados onde a memória é limitada.

Exemplo Prático: Servidor HTTP Concorrente

Vamos juntar tudo em um servidor HTTP que aceita múltiplas conexões:

const std = @import("std");
const Io = std.Io;
const net = std.net;

fn handleCliente(io: Io, conn: net.StreamServer.Connection) !void {
    defer conn.stream.close(io);

    var buf: [4096]u8 = undefined;
    const n = try conn.stream.read(io, &buf);

    const resposta =
        "HTTP/1.1 200 OK\r\n" ++
        "Content-Type: text/plain\r\n" ++
        "Content-Length: 11\r\n\r\n" ++
        "Olá, Zig!\n";

    try conn.stream.writeAll(io, resposta);
}

fn servidor(io: Io) !void {
    var server = net.StreamServer.init(io, .{});
    defer server.deinit(io);

    try server.listen(io, net.Address.parseIp("0.0.0.0", 8080));

    while (true) {
        const conn = try server.accept(io);
        // Cada cliente é tratado concorrentemente
        var _ = io.async(handleCliente, .{ io, conn });
    }
}

pub fn main() !void {
    // Escolha o modelo adequado ao seu ambiente
    var io = std.Io.greenThreads(.{ .entries = 256 });
    defer io.deinit();
    try servidor(io);
}

Compare isso com o tutorial de servidor HTTP que usa o modelo síncrono. A lógica é praticamente a mesma — a única diferença é o parâmetro io.

Function Color Blindness

O termo “function coloring” vem do famoso artigo de Bob Nystrom. Em Rust, funções async fn são “vermelhas” e funções normais são “azuis” — você não pode chamar uma vermelha de dentro de uma azul sem um runtime.

O Zig resolve isso completamente. Não existe distinção entre funções “async” e “sync”. Toda função que recebe Io pode ser usada em qualquer contexto. Isso elimina:

  • Duplicação de código (versão sync + async)
  • Incompatibilidade entre bibliotecas
  • Necessidade de escolher runtime na hora de compilar

Para quem vem de Rust, compare com o artigo Zig para desenvolvedores Rust. E se quiser entender como Go aborda concorrência de forma diferente, veja o modelo de goroutines em golang.com.br.

Impacto no Ecossistema

O novo std.Io muda fundamentalmente como bibliotecas Zig são escritas:

  1. Uma biblioteca, múltiplos runtimes — pacotes como httpz funcionam automaticamente com qualquer modelo de I/O
  2. Testabilidade — injete um Io mock nos testes para simular I/O sem tocar o disco
  3. Portabilidade — o mesmo código roda no Linux (io_uring), macOS (kqueue), Windows (IOCP) e WebAssembly
  4. Composição — combine bibliotecas que usam diferentes modelos sem conflito

Veja mais sobre o ecossistema de bibliotecas de rede do Zig e clientes HTTP.

Quando Usar Cada Modelo

CenárioModelo RecomendadoPor quê
CLI / ScriptsBlockingZero overhead, simplicidade
API RESTGreen ThreadsAlta concorrência, baixa latência
JogosThread PoolControle preciso de threads
WASMStacklessSem stack, compatível com browsers
EmbarcadosBlocking ou StacklessMemória limitada
MicroserviçosGreen ThreadsMilhares de conexões

Conclusão

O novo std.Io do Zig é uma das inovações mais importantes em design de linguagens de programação de sistemas. Ao tratar I/O como uma dependência injetável — assim como memória com allocators — o Zig elimina o problema de “function coloring” e permite que o mesmo código funcione de forma ótima em qualquer modelo de concorrência.

Se você quer se aprofundar, explore o tutorial de concorrência em Zig, o artigo sobre io_uring, e o cheatsheet de concorrência. Para comparar com a abordagem de Rust para async/await, veja nosso comparativo Zig vs Rust.

Também recomendamos o novo artigo sobre Zig para sistemas embarcados e IoT, onde o std.Io com modo stackless brilha especialmente.

Continue aprendendo Zig

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