- O autor, enquanto aprendia a linguagem Zig e trabalhava no projeto de reescrita do índice do AcoustID, tentou uma nova abordagem ao esbarrar nas limitações da programação de rede
- Para implementar em Zig o modelo de I/O assíncrono e concorrência que já usava em C++ e Go, decidiu desenvolver sua própria biblioteca
- Como resultado, criou a biblioteca Zio, que implementa em Zig um modelo de concorrência no estilo do Go, permitindo escrever código assíncrono com aparência de código síncrono, sem callbacks
- O Zio oferece suporte a I/O assíncrono de rede e arquivos, canais, primitivas de sincronização e monitoramento de sinais, e apresenta desempenho superior ao Go ou ao Tokio do Rust no modo single-thread
- Este projeto mostra a possibilidade de combinar o desempenho de nível de sistema do Zig com modelos modernos de concorrência, sendo visto como um ponto de virada importante para a expansão do ecossistema Zig
A linguagem Zig e a motivação inicial
- O autor já observava o Zig, originalmente projetado como uma linguagem de baixo nível para software de áudio, mas não sentia uma necessidade prática imediata
- Passou a se interessar ao ver o caso em que Andrew Kelley, criador do Zig, reimplementou em Zig o algoritmo Chromaprint do autor
- Conduziu o projeto de reescrita do índice invertido do AcoustID como uma oportunidade para aprender Zig e, como resultado, obteve uma implementação mais rápida e mais escalável do que a versão em C++
- Porém, ao adicionar a interface de servidor, encontrou o problema da falta de suporte adequado a rede assíncrona
Abordagens anteriores e limitações
- Na versão anterior em C++, o framework Qt era usado para tratar I/O assíncrono; embora baseado em callbacks, era viável graças ao suporte abrangente
- Em protótipos posteriores, aproveitou a conveniência da linguagem Go para rede e concorrência, mas em Zig faltava um nível semelhante de abstração
- Para implementar em Zig um servidor TCP e a camada de cluster, acabava surgindo a ineficiência de ter de criar muitas threads
- Para resolver isso, escreveu diretamente um cliente Zig para o sistema de mensageria NATS (
nats.zig) e explorou em profundidade os recursos de rede do Zig
O surgimento da biblioteca Zio
- Com base nessa experiência, publicou o Zio: biblioteca de I/O assíncrono e concorrência para Zig
- O objetivo do Zio é permitir escrever código assíncrono sem callbacks; internamente o I/O assíncrono funciona normalmente, mas externamente a estrutura se parece com código síncrono
- Implementa de forma limitada um modelo de concorrência no estilo do Go adaptado ao Zig
- As tarefas do Zio assumem a forma de corrotinas stackful com pilha de tamanho fixo
- Ao chamar
stream.read(), a operação de I/O é executada em segundo plano e, quando termina, a tarefa é retomada para retornar o resultado
- Essa abordagem oferece ao mesmo tempo simplificação do gerenciamento de estado e melhor legibilidade do código
Conjunto de recursos e estrutura do runtime
- O Zio suporta I/O assíncrono completo de rede e arquivos, primitivas de sincronização (mutex, variável de condição etc.), canais no estilo do Go e monitoramento de sinais do sistema operacional
- As tarefas podem ser executadas em modo single-thread ou multi-thread
- No modo multi-thread, as tarefas podem ser movidas entre threads, trazendo menor latência e melhor balanceamento de carga
- Implementa a interface padrão Reader/Writer, garantindo compatibilidade com bibliotecas externas
Desempenho e comparação
- O autor ainda não publicou benchmarks oficiais, mas afirma ter confirmado desempenho superior ao Go e ao Tokio do Rust no modo single-thread
- O custo de troca de contexto é tão baixo quanto uma chamada de função, oferecendo uma velocidade de troca praticamente gratuita
- O modo multi-thread ainda não é tão robusto quanto Go/Tokio, mas mostra desempenho semelhante ou ligeiramente superior
- No futuro, a adição de recursos de fairness pode reduzir um pouco esse desempenho
Código de exemplo e uso
- A documentação inclui um exemplo de servidor HTTP baseado em Zio
- Usa
zio.net.Stream para aceitar conexões e tratar cada conexão em uma tarefa separada
zio.Runtime gerencia a execução das tarefas e o agendamento de I/O
- Essa estrutura permite escrever I/O assíncrono como se fosse código síncrono, além de possibilitar controle de fluxo claro e gerenciamento da liberação de recursos
Planos futuros e significado
- Por meio do Zio, o autor concluiu que o Zig pode ir além de ser apenas uma linguagem para código de sistema de alto desempenho e evoluir para uma linguagem completa para desenvolvimento de aplicações de rede
- Como próximo passo, planeja reescrever o cliente NATS com base no Zio e desenvolver uma biblioteca cliente/servidor HTTP baseada em Zio
- O projeto é visto como uma iniciativa que impulsiona a expansão da infraestrutura de rede e concorrência do ecossistema Zig, além de representar uma tentativa de construir um modelo moderno de runtime comparável ao de Go ou Rust
1 comentários
Comentários do Hacker News
Não está claro se o design de async do Zig usa pares de call/return do hardware ou se é traduzido com base em saltos indiretos
Para fazer um benchmark realmente correto, seria preciso comparar o tempo total de execução entre um programa com troca contínua entre duas tarefas e um programa totalmente síncrono. Isso é bem complicado
Se você puder controlar o compilador, também é possível trocar call/ret do código de I/O por saltos explícitos
No longo prazo, seria desejável que as CPUs adotassem um meta-preditor para prever melhor corrotinas stackful
Texto relacionado: Zig new async I/O
Eu uso Zig em ambiente embarcado (ARM Cortex-M4, 256KB de RAM), e o utilizo para garantir segurança de memória na interoperabilidade com C
Prefiro um async com cores, como no Rust. Gosto dessa sensação mágica de parecer código síncrono, mas em codebases grandes o problema é ficar difícil distinguir quais funções fazem blocking
A CPU não bloqueia de fato em I/O, e as próprias threads do sistema operacional são corrotinas stackful implementadas pelo SO
No nível da linguagem, só é possível implementar essa ilusão de forma mais eficiente; a essência é a mesma
A cor é determinada por a função realizar ou não I/O, e no ponto de chamada fica explícito se é async ou não
O Zig também pretende calcular o tamanho de pilha necessário na chamada de função, então a expectativa é reduzir o desperdício de RAM das corrotinas stackful
Mesmo assim, o projeto continua muito ativo, e vejo de forma positiva o fato de priorizar o design correto acima de releases rápidas
Por enquanto uso Go ou C enquanto espero a 1.0
Eu também pretendo esperar a 0.16 para trabalhos focados em I/O
O código existente continua funcionando, e a nova API é mais ergonômica e performática
Eu mesmo migrei um projeto antigo para a nova API de Reader/Writer e o código ficou muito mais limpo
Uma abordagem como a do libtask parece muito mais limpa
O Rust também adotou async baseado em callback, e eu não entendo bem por quê
Referência: libtask
Mas mexer diretamente com a pilha pode entrar em conflito com tratamento de exceções, GC, depuradores etc.
Também é difícil incorporar esse tipo de mudança no nível do LLVM, então, do ponto de vista de quem projeta a linguagem, existem muitas restrições práticas
Se for pequeno demais, ocorre overflow; se for grande demais, há desperdício de memória
O tamanho de pilha necessário varia entre plataformas, então também há problemas de portabilidade
Quando a issue #157 do Zig for resolvida, essa abordagem deve melhorar
Ou seja, existem três formas de implementar async
O Rust converte isso em máquinas de estado estáticas, e o runtime faz o polling
O modelo stackful desperdiça muita memória e dificulta gerenciar o tamanho da pilha
Para evitar isso, o Rust adotou uma estrutura stackless, e o Zig deve permitir escolher entre os dois modelos
Referência: código de corrotinas do zio
setsockoptO Zig fornece uma camada de API POSIX
Referência: documentação do setsockopt
Estou imaginando uma estrutura que funcione como o
asyncio.timeoutdo PythonExemplo de código:
Na verdade, essa é a parte mais difícil
Referência: zio.dev
Mas o Zig, apesar de ser uma linguagem de baixo nível, me impressionou por conseguir expressar APIs de alto nível de forma elegante
Tanto Zig quanto Go ganharam novos bindings para Qt
Eu queria bindings para Rust. O cxx-qt é o único projeto que parece continuar mantido, mas eu não quero usar QML nem CMake. Quero usar Qt apenas com Rust + Cargo