4 pontos por GN⁺ 2025-12-04 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2025-12-04
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.Threaded por 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 para concurrent()

    • Sou o Daroc. Apliquei duas correções ao artigo para refletir esse feedback.
      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
    • Tenho uma pergunta para o Andrew.
      Gostaria de saber que tipo de bug acontece se alguém usar asyncConcurrent() por engano onde deveria usar async().
      Quero entender se isso pode virar UB (comportamento indefinido) dependendo do modelo de IO, ou se é apenas um erro de lógica
    • O bom de 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

    • Se chamar com argumentos diferentes já torna uma função “colorida”, então todas as funções são coloridas e o conceito perde o sentido ;)
      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
    • Na prática, o ponto central de function coloring é a duplicação de caminhos de código síncronos/assíncronos.
      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
    • O io do Zig não é um effect type contagioso.
      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)
    • Na prática, o Zig parece ter colorido tudo como async e deixado apenas a escolha de usar ou não threads de trabalho.
      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
    • Como desenvolvedor Haskell, isso me parece basicamente uma implementação de mônada IO sem suporte da linguagem
  • 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

    • Se você usar threads do SO diretamente, esbarra em limites de escalabilidade por causa da Lei de Little.
      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
    • Como referência, a ExecutionContext API do Scala ajuda a entender melhor os conceitos envolvidos
  • 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

    • Existem os built-ins de baixo nível @asyncSuspend e @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
    • No fim das contas, é possível que suspend/resume seja implementado como funções da biblioteca padrão em espaço de usuário.
      Pelo protótipo atual de Io.Evented, isso também pode ser tratado em bibliotecas de terceiros com base em corrotinas stackless
    • Também fico curioso se dá para implementar suspend/resume com apenas um pool de threads
    • Também questiono qual é o sentido de implementar corrotinas cooperativas como async preemptivo
  • 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 defer começar.
    Caso contrário, seria necessário rastrear a dependência entre createFile e writeAll.
    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

    var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
    var b_future = io.async(saveFile, .{io, data, "saveB.txt"});
    const a_result = a_future.await(io);
    const b_result = b_future.await(io);
    

    Em Rust ou Python, uma corrotina não avança se não for awaitada.
    Já no exemplo do Zig, se io.async avanç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

    • O C# também funciona de forma parecida. Uma função async roda na thread chamadora até o primeiro yield
    • No Zig também é necessário chamar .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
    • Na prática, a execução avança no momento do 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”
    • O JavaScript também funciona assim
  • 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

    • Foi apontado que encapsular todo IO em channels tem custo alto.
      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
    • O Zig tem algo parecido com os channels do Go em 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
    • Queria perguntar se você já usou a linguagem Odin. É um “better C” mais inspirado em Go do que em Zig
    • Gosto do fato de que, como no async/await do C#, não se força a existência de funções coloridas.
      Acho a abordagem “colorless” do Zig muito melhor
    • É um erro achar que o modelo de concorrência do Go é algo especial.
      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