⚠️ 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ística | Zig | JavaScript | Rust | Go |
|---|---|---|---|---|
| Runtime | Sem runtime obrigatório | V8 engine | Tokio/async-std | Goroutines |
| Stack | Configurável (async/await = structs) | Call stack + microtasks | Stackless coroutines | Growable stacks |
| Memory | Controle explícito | GC automático | Ownership | GC |
| Overhead | Zero (sem runtime) | Alto (event loop) | Médio | Médio |
| Color functions | Não (funções são agnósticas) | Sim (async vs sync) | Sim | Não |
O Que Torna Zig Único
- Sem “function coloring”: Em Zig, funções async e sync têm a mesma assinatura
- Sem runtime obrigatório: Você escolhe (ou implementa) seu event loop
- Zero overhead: Async é implementado como state machines, não threads
- Composição elegante:
async/await/suspend/resumefuncionam 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-chave | Função |
|---|---|
async | Inicia uma função async, retorna um frame |
await | Espera o frame completar, retorna o resultado |
suspend | Pausa a execução, retorna controle ao chamador |
resume | Retoma 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
libxevou implementações dostd.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ítica | Overhead de state machine não justifica |
| Precisa de controle fino de concorrência | Simplicidade é prioridade |
| Recursos são limitados | Time to market é crítico |
Comparação de Performance
| Operação | Síncrono | Async | Threads |
|---|---|---|---|
| Memória por conexão | ~0 bytes | ~100 bytes | ~1 MB |
| Context switch | N/A | ~50 ns | ~1-10 µs |
| Escalabilidade | Baixa | Alta | Média |
| Complexidade | Baixa | Média | Mé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:
- libxev — Event loop de alta performance
- aio — Abstração de I/O assíncrono
- 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/resumepara 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:
- 🌐 Criando um Servidor HTTP em Zig — Aplique async em servidores web
- 🧪 Testes em Zig: Guia Completo — Aprenda a testar código async
- 🔄 Comptime em Zig — Meta-programação com async
- 📚 libxev documentation — Event loop production-ready
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