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
        _ = 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

Perguntas frequentes sobre async no Zig

Zig tem async/await?

Sim. Após a reformulação com std.Io, o Zig voltou a ter concorrência baseada em futures, mas sem as keywords async/await fixas da linguagem que existiram até a versão 0.11. Hoje a concorrência é expressa pela API std.Io: você chama io.async(função, args) para iniciar uma tarefa e future.await(io) para aguardar o resultado. O modelo de execução — bloqueante, thread pool, green threads com io_uring ou coroutines stackless — é injetado em main, não fixado por função.

O que é o std.Io no Zig?

std.Io é a interface que centraliza toda a entrada e saída (arquivos, rede, timers e sincronização) e injeta o modelo de execução como parâmetro. Assim como o allocator é passado explicitamente em vez de usar um malloc global, o Io é recebido pela função. A mesma função roda de forma idêntica em modo síncrono, com thread pool ou com green threads, sem reescrever código.

Como o async do Zig difere do de Rust?

No Zig não existe function coloring: toda função que recebe Io pode ser chamada de qualquer contexto, síncrono ou assíncrono. No Rust, funções async fn são incompatíveis com funções comuns e exigem um runtime como tokio. O Zig elimina a duplicação (versão sync + async da mesma biblioteca) e a escolha de runtime acontece em tempo de execução, não de compilação. Para a comparação completa, veja Zig para desenvolvedores Rust e o comparativo Zig vs Rust.

Zig usa io_uring para async?

No Linux, o modelo std.Io.greenThreads aproveita io_uring para I/O assíncrono de verdade, com green threads leves que suportam milhares de conexões simultâneas e uso mínimo de memória — semelhante ao que o TigerBeetle faz em produção. Em outros sistemas, o Io usa os primitivos nativos (kqueue no macOS, IOCP no Windows) e também oferece um modo stackless compatível com WebAssembly.

É possível cancelar uma future em Zig?

Sim. Toda future retornada por io.async() suporta cancelamento via future.cancel(io). O padrão recomendado é defer future.cancel(io) catch {}, que garante que a operação seja cancelada e os recursos liberados se a função sair por erro, retorno antecipado ou errdefer — essencial para implementar timeouts e shutdown gracioso.

Async no Zig funciona em WebAssembly?

Sim, por meio do modelo std.Io.stackless(), que transforma futures em máquinas de estado sem stack. Isso torna a concorrência viável em ambientes sem stack como WebAssembly e em sistemas embarcados com memória limitada, mantendo o mesmo código usado no servidor.

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.