- 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
Comentarios en Hacker News
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
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
Artículo relacionado: Zig new async I/O
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
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 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
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
Yo también voy a esperar 0.16 para trabajos centrados en I/O
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
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
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
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
Es decir, hay tres formas de implementar async
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
setsockoptZig ofrece una capa de API POSIX
Referencia: documentación de setsockopt
std.Io.Readerde Zig no entiende timeoutsEstoy pensando en una estructura que funcione como
asyncio.timeoutde PythonCódigo de ejemplo:
De hecho, esa es la parte más difícil
Referencia: zio.dev
Pero Zig me impresionó porque, pese a ser un lenguaje de bajo nivel, puede expresar APIs de alto nivel de forma limpia
Tanto Zig como Go tienen nuevos bindings de Qt
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