- A Linguagem Zig introduziu um novo modelo baseado na interface
Io para reduzir a complexidade do design de E/S assíncrona anterior
- Esse modelo mantém a mesma estrutura de função sem distinção entre código síncrono e assíncrono, e oferece as implementações
Io.Threaded e Io.Evented
Io.Threaded executa de forma síncrona por padrão, enquanto Io.Evented executa de forma assíncrona baseada em event loop
- Os desenvolvedores podem controlar execução paralela com as funções
async() e concurrent() sem precisar alterar o código, permitindo otimização de desempenho
- A abordagem resolve o problema de
function coloring e busca manter a simplicidade e controlabilidade do Zig enquanto garante desempenho assíncrono
Mudanças no design assíncrono do Zig
- O Zig está buscando uma nova abordagem porque o design assíncrono antigo não se alinhava bem com a filosofia de minimalismo da linguagem
- O design anterior tinha baixa integração com outros recursos
- O novo modelo permite lidar com I/O síncrona e assíncrona na mesma estrutura de código
- O novo design funciona em torno da interface genérica
Io
- Todas as funções de I/O recebem uma instância de
Io como parâmetro para execução
- É uma estrutura semelhante à interface
Allocator, permitindo controle de I/O de forma parecida ao controle de alocação de memória
Estrutura da interface Io
- A biblioteca padrão inclui duas implementações básicas
Io.Threaded: execução síncrona por padrão, com paralelização por threads quando necessário
Io.Evented: execução assíncrona baseada em event loop (usa io_uring, kqueue etc.)
- Os usuários podem criar suas próprias implementações de
Io, permitindo controle granular do modo de execução
Exemplo de código e funcionamento
- A função de exemplo
saveFile() cria arquivo, grava e fecha
- Com
Io.Threaded, a execução ocorre por chamadas de sistema normais
- Com
Io.Evented, a execução ocorre com backend assíncrono
- Em ambos os casos,
writeAll() garante que a operação esteja concluída no momento da chamada
- O mesmo código funciona igualmente em ambientes síncronos e assíncronos
- Desenvolvedores de bibliotecas não precisam se preocupar com o modo de execução
Execução paralela com async() / concurrent()
- A função
async() solicita execução assíncrona, mas em Io.Threaded ela pode ser executada imediatamente
- Em
Io.Evented, é possível salvar dois arquivos ao mesmo tempo como execução realmente assíncrona
- A função
concurrent() é usada quando a execução paralela real é necessária
Io.Threaded utiliza pool de threads
Io.Evented trata como async()
- Uma escolha incorreta de função (
concurrent em vez de async) é considerada bug, e não pode ser evitada em nível de linguagem
Estilo de código e integração com a linguagem
- Mantém o estilo de código Zig padrão sem sintaxe assíncrona dedicada
- As construções existentes de fluxo de controle, como
try e defer, continuam iguais
- Andrew Kelley observou que “parece leitura de código Zig padrão”
- Um exemplo de implementação de lookup DNS assíncrono é apresentado
- Diferente de
getaddrinfo(), retorna apenas a primeira resposta de sucesso e cancela as demais requisições
Planos futuros e status de desenvolvimento
Io.Evented ainda está em fase experimental, com falta de suporte em alguns sistemas operacionais
- Há plano para uma implementação de
Io compatível com WebAssembly, com o desenvolvimento de recursos correlatos ainda necessário
- Existem 24 tarefas de acompanhamento relacionadas ao
Io, com a maioria ainda não concluída
- O Zig ainda está pré-1.0, e I/O assíncrono e geração de código nativo permanecem como tarefas principais
- Com este design, espera-se redução na frequência de reescrita de código causada por mudanças na interface de I/O
Resumo da discussão da comunidade
- Em vários comentários, a abordagem do Zig foi avaliada como mais simples e flexível que o modelo async/await do Rust
- O Rust pode ficar complexo quando combina múltiplos executors
- O Zig garante a possibilidade de coexistência de múltiplos executors com a interface
Io
- Alguns apontaram que o código pode ficar um pouco prolixo
- Porém, com uma API explícita há melhoria em segurança, desempenho e controle de testes
- Debate técnico também abordou diferenças entre execução assíncrona e por thread, e implementação de stackful vs stackless coroutine
- O
Io do Zig é implementado como expansão da biblioteca padrão, sem tratamento especial na linguagem
- Funcionalidade de stackless coroutine está planejada para o futuro
Conclusão
- O novo modelo assíncrono do Zig visa manter a simplicidade da linguagem e atingir alto desempenho de I/O
- Ao resolver o problema de
function coloring, integrar código síncrono e assíncrono e usar uma estrutura de controle explícita, foi visto como uma etapa importante para a estabilização do Zig 1.0
1 comentários
Comentários do Hacker News
No geral, este texto está correto e bem pesquisado.
Só há alguns pequenos ajustes.
Em uma instância de
Io.Threaded,async()na verdade não opera de forma assíncrona e é executado imediatamente. Ainda assim,std.Io.Threadedpor padrão usa um pool de threads para distribuir trabalho assíncrono.Porém, se for inicializado com
init_single_threaded, ele se comporta como descrito no artigo.Além disso, antigamente existia uma função chamada
asyncConcurrent(), mas agora ela foi simplesmente renomeada paraconcurrent()Se quiser enviar feedback no futuro, pode mandar e-mail para lwn@lwn.net.
Obrigado pela sugestão de correção e pelo trabalho relacionado a Zig
Gostaria de saber que tipo de bug acontece se alguém usar
asyncConcurrent()por engano onde deveria usarasync().Quero entender se isso pode virar UB (comportamento indefinido) dependendo do modelo de IO, ou se é apenas um erro de lógica
concurrent()é que ele melhora a legibilidade e expressividade do código, deixando claro que “este código precisa necessariamente rodar em paralelo”Acho esse design bem razoável.
Mas a explicação do Zig é confusa.
Eles enfatizam que resolveram o problema de function coloring, mas na prática apenas empurraram IO para dentro de um tipo de efeito (effect type).
Isso exige que o chamador mantenha um token, então ainda é uma forma de coloring.
Vejo isso como algo semelhante à forma como Go trata assíncrono
O modelo antigo de async-await do Zig já resolvia o problema de coloring.
Isso porque o compilador gerava automaticamente versões síncronas/assíncronas de acordo com o contexto da chamada
O Zig resolve isso com injeção de dependência, o que é suficiente na prática.
A complexidade de chamadas async é inevitável, mas faz parte do custo necessário para ter controle fino
Dá para declarar uma variável global de io e usá-la em qualquer lugar (embora isso não seja recomendado ao escrever bibliotecas).
Se você olhar o artigo What color is your function?, que resume cinco condições do problema de function coloring, é bem provável que a abordagem do Zig não satisfaça algumas delas (especialmente 4 e 5)
Mas essa abordagem pode causar problemas como deadlock.
Algumas partes do código não são thread-safe, então o coloring às vezes ajuda
Esse design parece muito parecido com o async do Scala.
No Scala, o contexto de execução é passado como parâmetro implícito; no Zig, ele é explícito.
Na prática, isso não era muito melhor do que usar threads e filas diretamente, e o gerenciamento do contexto de execução causava complexidade e comportamentos imprevisíveis.
Parece que a equipe do Zig não tem muita experiência com Scala e achou essa abordagem algo novo
A JVM resolve isso com threads virtuais, mas linguagens de baixo nível têm mais dificuldade para alcançar a mesma eficiência.
Então uma linguagem como Zig precisa de outra solução de escalabilidade
No antigo sistema async/await do Zig, era possível fazer suspend/resume de funções.
Eu queria usar isso no desenvolvimento de SO para implementar suspensão/retomada de frames com base em interrupções de dispositivo.
É uma pena que no novo sistema de io pareça que isso vai precisar ser implementado manualmente
@asyncSuspende@asyncResume.O novo Io é uma abstração comum para modos síncrono, com threads e baseado em eventos, então o mecanismo de suspend não está incluído
Pelo protótipo atual de Io.Evented, isso também pode ser tratado em bibliotecas de terceiros com base em corrotinas stackless
No código de exemplo, foi dito que quando
writeAll()retorna o trabalho está concluído,mas como a implementação de IO pode variar, na prática a conclusão deveria ser garantida quando o
defercomeçar.Caso contrário, seria necessário rastrear a dependência entre
createFileewriteAll.Nesse caso, no fim das contas isso não parece muito diferente de uma chamada bloqueante.
Além disso, também não está claro por que essa interface se chama IO.
Na prática, ela parece mais uma abstração para “executar em outro contexto”
Documentação relacionada: std.Io
O exemplo a seguir é interessante
Em Rust ou Python, uma corrotina não avança se não for awaitada.
Já no exemplo do Zig, se
io.asyncavançar por conta própria, isso se parece mais com criação de tarefas.É um design válido, mas não foi o caminho escolhido por outras linguagens
asyncroda na thread chamadora até o primeiro yield.await(io)para que a execução seja garantida.Se ela vai rodar imediatamente ou entrar na fila do pool de threads depende da implementação do runtime de Io
await.No io baseado em eventos, os dois trabalhos podem rodar de forma intercalada (interleaved), e no io com threads podem prosseguir em segundo plano.
Ou seja, não existem “tarefas rodando escondidas em algum lugar”
Como alguém que usa Go todos os dias, sinto que o Io do Zig corrige várias deficiências do Go.
Mas fico curioso se o Zig tem um conceito de channel.
No Go existe a palavra-chave select, mas sempre achei uma pena ela não funcionar com sockets
Os channels do Go têm overhead de dezenas de ciclos, então são ineficientes para IO de granularidade pequena.
Por outro lado, são úteis para movimentação de dados em blocos maiores ou sincronização muitos-para-muitos
std.Io.Queue.Também dá para implementar algo semelhante a select, embora a sintaxe seja menos ergonômica.
Em compensação, isso pode funcionar em vários runtimes de IO sem GC
Acho a abordagem “colorless” do Zig muito melhor
Goroutines são apenas green threads, channels são apenas filas thread-safe, e o Zig já oferece isso na biblioteca padrão
A versão async de Io no Zig parece quase idêntica à abordagem do Go.
A diferença é que, no Go, chamadas para bibliotecas C têm alto custo de alocação de pilha, e fazer syscalls diretamente traz problemas de compatibilidade entre plataformas.
O Zig aparentemente tornou isso configurável, permitindo escolher diferentes trade-offs sem mudar o código
O novo async IO é excelente em exemplos simples, mas pode ter limitações em IO complexo em nível de servidor.
Registrei questões relacionadas no GitHub
O problema central é que os projetistas da linguagem ou da biblioteca precisam oferecer um meio de conectar contextos de execução diferentes (sync/async).
Para isso, é necessário encapsular o contexto em uma FSM (máquina de estados finitos) e fornecer um canal de comunicação entre os dois lados
Artigo relacionado: Function colors represent different execution contexts