- L’auteur, qui apprenait le langage Zig tout en menant un projet de réécriture de l’index AcoustID, a tenté une nouvelle approche après s’être heurté aux limites de la programmation réseau
- Pour reproduire dans Zig les modèles d’E/S asynchrones et de concurrence qu’il utilisait auparavant en C++ et en Go, il a décidé de développer sa propre bibliothèque
- Le résultat est Zio, une bibliothèque qui implémente dans Zig un modèle de concurrence de style Go, permettant d’écrire du code asynchrone sans callbacks, avec une apparence synchrone
- Zio prend en charge les E/S réseau et fichier asynchrones, les canaux, les primitives de synchronisation, la surveillance des signaux et plus encore, avec des performances supérieures en mode mono-thread à Go ou à Tokio de Rust
- Ce projet montre la possibilité de combiner les performances de niveau système de Zig avec un modèle de concurrence moderne, et est considéré comme un tournant important pour l’expansion de l’écosystème Zig
Le langage Zig et la motivation initiale
- L’auteur suivait depuis longtemps Zig, à l’origine conçu comme un langage bas niveau pour les logiciels audio, sans toutefois ressentir de besoin concret de l’utiliser
- Son intérêt s’est renforcé après avoir vu Andrew Kelley, créateur de Zig, réimplémenter son algorithme Chromaprint en Zig
- Il a profité du projet de réécriture de l’index inverse d’AcoustID pour apprendre Zig, et a finalement obtenu une implémentation plus rapide et plus scalable que la version C++
- Mais lors de l’ajout de l’interface serveur, il s’est heurté à un manque de support pour le réseau asynchrone
Approches précédentes et leurs limites
- Dans la version C++ précédente, les E/S asynchrones étaient gérées via le framework Qt ; l’approche était basée sur des callbacks, mais restait utilisable grâce à la richesse du support
- Dans un prototype ultérieur, il avait exploité la simplicité du réseau et de la concurrence en Go, mais Zig ne proposait pas un niveau d’abstraction comparable
- Pour implémenter en Zig un serveur TCP et une couche cluster, il aurait fallu créer un grand nombre de threads, ce qui s’est révélé inefficace
- Pour résoudre cela, il a écrit lui-même un client Zig pour le système de messagerie NATS (
nats.zig) et a ainsi exploré en profondeur les capacités réseau de Zig
L’arrivée de la bibliothèque Zio
- Fort de cette expérience, il a publié Zio : une bibliothèque d’E/S asynchrones et de concurrence pour Zig
- Zio vise à écrire du code asynchrone sans callbacks ; en interne, les E/S restent asynchrones, mais la structure apparaît comme synchrone de l’extérieur
- Il s’agit d’une implémentation limitée d’un modèle de concurrence de style Go adaptée à Zig
- Les tâches de Zio prennent la forme de coroutines stackful avec une pile de taille fixe
- Lors d’un appel à
stream.read(), l’opération d’E/S est exécutée en arrière-plan, puis la tâche reprend une fois terminée pour renvoyer le résultat
- Cette approche simplifie à la fois la gestion de l’état et la lisibilité du code
Ensemble de fonctionnalités et structure du runtime
- Zio prend en charge les E/S réseau et fichier entièrement asynchrones, les primitives de synchronisation (mutex, variables de condition, etc.), les canaux de style Go, la surveillance des signaux OS, etc.
- Les tâches peuvent s’exécuter en mode mono-thread ou multi-thread
- En mode multi-thread, les tâches peuvent se déplacer entre les threads, ce qui réduit la latence et améliore l’équilibrage de charge
- Zio implémente les interfaces standard Reader/Writer, assurant la compatibilité avec des bibliothèques externes
Performances et comparaison
- L’auteur n’a pas encore publié de benchmark officiel, mais indique avoir constaté des performances supérieures en mode mono-thread à Go et à Tokio de Rust
- Le coût du changement de contexte est aussi faible qu’un appel de fonction, offrant des transitions pratiquement gratuites
- Le mode multi-thread n’est pas encore aussi robuste que Go/Tokio, mais affiche des performances comparables ou légèrement supérieures
- L’ajout futur d’une fonctionnalité de fairness pourrait entraîner une légère baisse des performances
Exemples de code et usages
- La documentation inclut un exemple de serveur HTTP basé sur Zio
zio.net.Stream est utilisé pour accepter les connexions, chaque connexion étant traitée dans une tâche distincte
zio.Runtime gère l’exécution des tâches et l’ordonnancement des E/S
- Cette structure permet d’écrire des E/S asynchrones comme du code synchrone, avec un contrôle de flux clair et une gestion explicite de la libération des ressources
Perspectives et portée
- Grâce à Zio, l’auteur estime que Zig peut aller au-delà d’un simple langage pour du code système haute performance et évoluer vers un langage complet pour le développement d’applications réseau
- La prochaine étape consiste à réécrire le client NATS sur la base de Zio et à développer des bibliothèques client/serveur HTTP basées sur Zio
- Ce projet est vu comme une tentative majeure d’étendre l’infrastructure réseau et de concurrence de l’écosystème Zig, en construisant un modèle de runtime moderne comparable à ceux de Go ou Rust
1 commentaires
Avis Hacker News
On ne sait pas clairement si la conception async de Zig utilise des paires call/return matérielles, ou si elle est traduite sur la base de sauts indirects
Pour faire un benchmark rigoureux, il faudrait comparer le temps d’exécution total d’un programme avec des bascules continues entre deux tâches et celui d’un programme entièrement synchrone. C’est assez délicat
Si l’on peut contrôler le compilateur, il est aussi possible de remplacer les call/ret du code I/O par des sauts explicites
À long terme, on aimerait que les CPU introduisent un méta-prédicteur (meta-predictor) pour mieux prédire les coroutines avec pile
Article lié : Zig new async I/O
J’utilise Zig en environnement embarqué (ARM Cortex-M4, 256KB RAM), et je m’en sers pour garantir la sécurité mémoire dans l’interopérabilité avec le C
Je préfère l’async coloré à la Rust. L’aspect magique qui donne l’impression de code synchrone est agréable, mais dans une grosse base de code, le problème est qu’il devient difficile de distinguer quelles fonctions sont bloquantes
Le CPU ne se bloque pas réellement sur les I/O, et les threads OS eux-mêmes sont des coroutines stackful implémentées par l’OS
Le langage peut simplement implémenter cette illusion de manière plus efficace, mais le fond reste le même
La couleur dépendra du fait qu’une fonction effectue ou non des I/O, et l’appel indiquera explicitement s’il est async
Zig vise aussi à calculer la taille de pile nécessaire à l’appel d’une fonction, ce qui devrait réduire le problème de gaspillage de RAM des coroutines stackful
Mais le projet reste très actif, et j’apprécie qu’il privilégie une bonne conception plutôt qu’une sortie rapide
Pour l’instant j’utilise Go ou C en attendant la 1.0
Moi aussi, j’attendrai la 0.16 pour les travaux centrés sur les I/O
Le code existant continue de fonctionner, et la nouvelle API est plus ergonomique et performante
J’ai moi-même migré un projet existant vers la nouvelle API Reader/Writer, et le code est devenu bien plus propre
Une approche comme libtask semble bien plus propre
Rust a aussi adopté un async basé sur les callbacks, et je ne comprends pas bien pourquoi
Référence : libtask
Mais manipuler directement la pile peut entrer en conflit avec la gestion des exceptions, le GC, le débogueur, etc.
Il est aussi difficile de fusionner ce genre de changement au niveau LLVM, donc du point de vue des concepteurs de langage, il y a beaucoup de contraintes réalistes
Trop petite, elle déborde ; trop grande, elle gaspille de la mémoire
La taille de pile nécessaire varie aussi selon les plateformes, ce qui pose un problème de portabilité
Si l’issue Zig #157 est résolue, cette approche pourrait devenir meilleure
Autrement dit, il existe trois façons d’implémenter l’async
Rust est transformé en machine à états statique que le runtime interroge par polling
Le modèle stackful gaspille beaucoup de mémoire et complique la gestion de la taille de pile
Pour éviter cela, Rust a adopté une structure stackless, et Zig devrait permettre de choisir entre les deux approches
Référence : code coroutine de zio
setsockoptZig fournit une couche d’API POSIX
Référence : documentation setsockopt
std.Io.Readerde Zig n’a pas conscience des timeoutsUne structure fonctionnant comme
asyncio.timeoutde Python est à l’étudeExemple de code :
En réalité, c’est la partie la plus difficile
Référence : zio.dev
Mais Zig m’a impressionné par sa capacité à exprimer proprement des API de haut niveau tout en restant un langage bas niveau
Zig comme Go ont désormais de nouveaux bindings Qt
Je voudrais des bindings pour Rust. cxx-qt est le seul projet encore maintenu, mais je ne veux utiliser ni QML ni CMake. Je veux utiliser Qt uniquement avec Rust + Cargo