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:
- Uma biblioteca, múltiplos runtimes — pacotes como httpz funcionam automaticamente com qualquer modelo de I/O
- Testabilidade — injete um
Iomock nos testes para simular I/O sem tocar o disco - Portabilidade — o mesmo código roda no Linux (io_uring), macOS (kqueue), Windows (IOCP) e WebAssembly
- 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ário | Modelo Recomendado | Por quê |
|---|---|---|
| CLI / Scripts | Blocking | Zero overhead, simplicidade |
| API REST | Green Threads | Alta concorrência, baixa latência |
| Jogos | Thread Pool | Controle preciso de threads |
| WASM | Stackless | Sem stack, compatível com browsers |
| Embarcados | Blocking ou Stackless | Memória limitada |
| Microserviços | Green Threads | Milhares 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.