2 puntos por GN⁺ 2025-10-28 | 1 comentarios | Compartir por WhatsApp
  • El autor, mientras aprendía el lenguaje Zig y avanzaba en el proyecto de reescritura del índice de AcoustID, intentó un nuevo enfoque a partir de las limitaciones que encontró en la programación de redes
  • Para implementar en Zig el modelo de E/S asíncrona y concurrencia que usaba antes en C++ y Go, decidió desarrollar su propia biblioteca
  • Como resultado, creó la biblioteca Zio, que implementa en Zig un modelo de concurrencia estilo Go adaptado al lenguaje, permitiendo escribir código asíncrono que se ve como si fuera síncrono, sin callbacks
  • Zio soporta E/S asíncrona de red y archivos, canales, primitivas de sincronización, monitoreo de señales y más, y en modo de un solo hilo muestra un rendimiento superior al de Go o Tokio de Rust
  • Este proyecto muestra la posibilidad de combinar el rendimiento a nivel de sistema de Zig con un modelo moderno de concurrencia, y se considera un punto de inflexión importante para la expansión del ecosistema de Zig

Zig y la motivación inicial

  • El autor había seguido de cerca a Zig, originalmente diseñado como un lenguaje de bajo nivel para software de audio, pero no había sentido una necesidad práctica de usarlo
    • Empezó a interesarse después de ver que Andrew Kelley, creador de Zig, reimplementó en Zig el algoritmo Chromaprint del autor
  • Tomó el proyecto de reescritura del índice invertido de AcoustID como una oportunidad para aprender Zig, y logró una implementación más rápida y escalable que la versión en C++
  • Sin embargo, al llegar a la etapa de agregar la interfaz del servidor, se topó con la falta de soporte para redes asíncronas

Enfoques previos y sus límites

  • En la versión anterior en C++, usaba el framework Qt para manejar E/S asíncrona; estaba basada en callbacks, pero era usable gracias a su soporte amplio
  • Más adelante, en un prototipo, aprovechó la comodidad de redes y concurrencia de Go, pero en Zig no existía un nivel similar de abstracción
  • Para implementar en Zig un servidor TCP y una capa de clúster, surgía la ineficiencia de tener que crear muchos hilos
    • Para resolverlo, escribió directamente un cliente de Zig para el sistema de mensajería NATS (nats.zig) y exploró a fondo las capacidades de red de Zig

La llegada de la biblioteca Zio

  • A partir de esa experiencia, publicó Zio: una biblioteca de E/S asíncrona y concurrencia para Zig
  • Zio tiene como objetivo permitir escribir código asíncrono sin callbacks; internamente la E/S asíncrona sigue funcionando, pero desde fuera la estructura se ve como síncrona
  • Implementa de forma acotada un modelo de concurrencia estilo Go adaptado a Zig
    • Las tareas de Zio toman la forma de corutinas stackful con pilas de tamaño fijo
    • Cuando se llama a stream.read(), la operación de E/S se ejecuta en segundo plano y, al completarse, la tarea se reanuda y devuelve el resultado
  • Este enfoque ofrece al mismo tiempo simplificación en el manejo de estado y mejor legibilidad del código

Funcionalidad y estructura del runtime

  • Zio soporta E/S asíncrona completa de red y archivos, primitivas de sincronización (mutex, variables de condición, etc.), canales estilo Go, monitoreo de señales del SO y más
  • Las tareas pueden ejecutarse en modo de un solo hilo o multihilo
    • En modo multihilo, las tareas pueden moverse entre hilos, con efectos de menor latencia y mejor balance de carga
  • Implementa la interfaz estándar Reader/Writer, asegurando compatibilidad con bibliotecas externas

Rendimiento y comparación

  • El autor todavía no ha publicado benchmarks oficiales, pero menciona haber confirmado un rendimiento superior al de Go y Tokio de Rust en modo de un solo hilo
  • El costo del cambio de contexto es tan bajo como una llamada de función, ofreciendo una velocidad de cambio prácticamente gratuita
  • El modo multihilo todavía no es tan robusto como Go/Tokio, pero muestra un rendimiento similar o ligeramente superior
    • En el futuro, agregar funciones de fairness podría reducir algo el rendimiento

Código de ejemplo y uso

  • La documentación incluye código de ejemplo de un servidor HTTP basado en Zio
    • Usa zio.net.Stream para aceptar conexiones y manejar cada una en una tarea separada
    • zio.Runtime se encarga de la ejecución de tareas y de la planificación de E/S
  • Esta estructura permite escribir E/S asíncrona como si fuera código síncrono, con control de flujo claro y gestión de liberación de recursos

Planes futuros y relevancia

  • A través de Zio, el autor confirma que Zig puede evolucionar más allá de ser solo un lenguaje para código de sistemas de alto rendimiento y convertirse en un lenguaje completo para desarrollar aplicaciones de red
  • Como siguiente paso, planea reescribir el cliente de NATS sobre Zio y desarrollar una biblioteca cliente/servidor HTTP basada en Zio
  • Este proyecto impulsa la expansión de la infraestructura de redes y concurrencia del ecosistema Zig, y se valora como un intento de construir un modelo moderno de runtime comparable al de Go o Rust

1 comentarios

 
GN⁺ 2025-10-28
Comentarios en Hacker News
  • Se dice que el cambio de contexto es casi gratis al nivel de llamada de función, pero en la práctica hay costos sutiles, como romper el predictor de saltos (branch predictor)
    No está claro si el diseño async de Zig usa pares call/return del hardware o si se traduce con saltos indirectos
    Para hacer un benchmark correcto, habría que comparar el tiempo total de ejecución entre un programa con cambios constantes entre dos tareas y otro completamente sincrónico. Eso es bastante complicado
    • En corutinas stackless, si se alterna continuamente entre dos tareas al fondo de la pila de llamadas y el código de cambio de pila está inlineado, se puede evitar en gran parte la penalización por desajuste de call/ret
      Si se pudiera controlar el compilador, también sería posible cambiar los call/ret del código de I/O por saltos explícitos
      A largo plazo, ojalá los CPU incorporen un meta-predictor que prediga mejor las corutinas stackful
    • En Zig, por ahora async desapareció del lenguaje, y el OP implementó directamente el cambio de tareas en espacio de usuario
    • Cuando hice una prueba simple de ping-pong entre corutinas, obtuve cifras difíciles de creer en comparación con otras soluciones
    • Pronto se agregará un nuevo async a Zig, así que estoy esperando antes de profundizar en serio
      Artículo relacionado: Zig new async I/O
  • Las corutinas stackful tienen sentido cuando hay suficiente RAM
    Yo uso Zig en un entorno embebido (ARM Cortex-M4, 256KB RAM), y lo uso para asegurar seguridad de memoria al interoperar con C
    Personalmente prefiero un async con color como en Rust. Me gusta esa sensación mágica de que parece código sincrónico, pero en bases de código grandes el problema es que se vuelve difícil distinguir qué funciones hacen blocking
    • En realidad, todo código sincrónico es una ilusión creada por el software
      El CPU no se bloquea realmente en I/O, y los hilos del sistema operativo son en sí mismos corutinas stackful implementadas por el OS
      A nivel de lenguaje solo se puede implementar esa ilusión de forma más eficiente; en esencia es lo mismo
    • El nuevo IO de Zig va a tener una estructura coloreada más elegante que la de Rust
      El color se decide según si la función realiza I/O, y al momento de llamarla se indica explícitamente si es async
      Zig también apunta a poder calcular el tamaño de pila necesario en las llamadas a función, así que se espera que eso reduzca el problema del desperdicio de RAM en corutinas stackful
    • Justamente por eso Zig quiere expresar I/O de forma explícita: para poder rastrear qué funciones bloquean
  • Hay quien opina que todavía es demasiado pronto para adoptar Zig. Da la impresión de que el modelo de I/O está cambiando mucho y que tomará algunos años
    • Yo también dejé Zig en 2020 por una razón parecida.
      Aun así, el proyecto sigue muy activo, y me parece positivo que priorice el diseño correcto antes que lanzar rápido
      Por ahora uso Go o C mientras espero la 1.0
    • Unos años pasan rápido. Zig ya es un lenguaje suficientemente usable. El que quiera usarlo, lo usa; el que no, no
    • En realidad sí es un mal momento. En 0.16 se espera un gran cambio de I/O, y ni siquiera el autor está usando aún las funciones más recientes
      Yo también voy a esperar 0.16 para trabajos centrados en I/O
    • Pero si el trabajo está relacionado con I/O, usar la interfaz de reader/writer con buffer de Zig 0.15 probablemente no implique cambios grandes
    • Yo más bien creo lo contrario. Zig no está cambiando bruscamente como lenguaje, sino que está añadiendo una nueva y potente API std.Io
      El código existente sigue funcionando igual, y la nueva API es más ergonómica y con mejor rendimiento
      Yo también migré un proyecto existente a la nueva API Reader/Writer y el código quedó mucho más limpio
  • Sigue siendo un misterio por qué el async basado en callbacks se volvió el estándar
    Un enfoque como libtask parece mucho más limpio
    Rust también adoptó un async basado en callbacks, y no entiendo muy bien por qué
    Referencia: libtask
    • Las corutinas stackless pueden implementarse dentro del lenguaje, y su ventaja es la interacción predecible con las funciones existentes
      Pero si uno manipula la pila directamente, puede chocar con manejo de excepciones, GC, depuradores, etc.
      Además, es difícil integrar este tipo de cambios al nivel de LLVM, así que desde la perspectiva del diseñador del lenguaje hay muchas restricciones prácticas
    • Según la investigación que hizo Microsoft para el estándar de C++, las corutinas stackless tienen mucho menos sobrecosto de memoria y dan más libertad en el diseño del ejecutor
    • La desventaja de enfoques como zio o libtask es que hay que estimar manualmente el tamaño de la pila
      Si es muy pequeña, hay overflow; si es muy grande, se desperdicia memoria
      Además, el tamaño de pila necesario varía según la plataforma, lo que también genera problemas de portabilidad
      Si se resuelve el issue #157 de Zig, este enfoque podría mejorar bastante
    • En casos como libtask, el tamaño de la pila del hilo es ambiguo y mucho mayor que el estado async típico
    • El async de Rust no está basado en callbacks, sino en polling
      Es decir, hay tres formas de implementar async
      1. Basado en callbacks (Node.js, Swift)
      2. Basado en stackful (Go, libtask)
      3. Basado en polling (Rust)
        Rust se transforma en una máquina de estados estática y el runtime la sondea
        El enfoque stackful desperdicia mucha memoria y hace difícil gestionar el tamaño de la pila
        Para evitar eso, Rust adoptó una estructura stackless, y Zig planea permitir elegir ambos enfoques
        Referencia: código de corutinas de zio
  • Una lectura TCP podría bloquearse durante un mes, así que me pregunto cómo será la interfaz de timeout de I/O
    • En sockets TCP se pueden configurar timeouts de lectura/escritura con setsockopt
      Zig ofrece una capa de API POSIX
      Referencia: documentación de setsockopt
    • Actualmente, std.Io.Reader de Zig no entiende timeouts
      Estoy pensando en una estructura que funcione como asyncio.timeout de Python
      Código de ejemplo:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • La mayoría de los frameworks async pasan por alto los timeouts y la cancelación
      De hecho, esa es la parte más difícil
  • En Scala ya existe una biblioteca de concurrencia llamada ZIO
    Referencia: zio.dev
  • Recientemente me impresionó Tokio de Rust, y si en Zig se pudiera implementar concurrencia estilo Go sin GC, definitivamente me gustaría probarlo
    • Go puede usar trucos como pilas infinitamente escalables gracias al GC
      Pero Zig me impresionó porque, pese a ser un lenguaje de bajo nivel, puede expresar APIs de alto nivel de forma limpia
  • Conocí Zig por primera vez en el sitio web de Bun. Últimamente está avanzando muy rápido
  • En una versión anterior en C++ implementé I/O asíncrono con Qt, pero esta vez cambié a Go
    Tanto Zig como Go tienen nuevos bindings de Qt
    • Go: miqt
    • Zig: libqt6zig
      Yo quisiera bindings para Rust. cxx-qt es el único proyecto que sigue con mantenimiento, pero no quiero usar QML ni CMake. Quiero usar Qt solo con Rust + Cargo
  • En Scala también ya existe el conocido framework ZIO, así que pensar nombres realmente es difícil