- Le langage Zig introduit un nouveau modèle basé sur l’interface
Iopour réduire la complexité du design asynchrone d’I/O existant - Ce modèle conserve une structure de fonction identique sans distinguer code synchrone et asynchrone, et propose deux implémentations :
Io.ThreadedetIo.Evented Io.Threadedexécute par défaut de manière synchrone, tandis queIo.Eventedexécute en mode asynchrone basé sur une boucle d’événements- Les développeurs peuvent contrôler l’exécution parallèle via
async()etconcurrent(), avec une optimisation des performances possible sans modifier le code - Cette approche résout le problème de la coloration de fonctions et vise à préserver la simplicité et le contrôle de Zig tout en offrant de hautes performances asynchrones
Évolution de la conception asynchrone de Zig
- Zig, estimant que l’ancien modèle asynchrone ne s’accordait pas bien avec sa philosophie de minimalisme, a recherché une nouvelle approche
- L’ancien modèle avait une faible intégration avec le reste des fonctionnalités
- Le nouveau modèle permet de gérer l’I/O synchrone et asynchrone avec la même structure de code
- Le nouveau modèle est centré sur une interface générique
Io- Toutes les fonctions d’I/O reçoivent une instance
Ioen paramètre pour s’exécuter - À l’image de l’interface
Allocator, il permet de piloter l’I/O de la même manière que l’allocation mémoire
- Toutes les fonctions d’I/O reçoivent une instance
Structure de l’interface Io
- La bibliothèque standard inclut deux implémentations de base
Io.Threaded: exécution synchrone par défaut, avec traitement parallèle par threads si nécessaireIo.Evented: exécution asynchrone basée sur une boucle d’événements (io_uring,kqueue, etc.)
- Les utilisateurs peuvent écrire de nouvelles implémentations
Io, pour un contrôle fin du mode d’exécution
Exemple de code et fonctionnement
- La fonction d’exemple
saveFile()crée, écrit et ferme un fichier- Avec
Io.Threaded, elle s’exécute avec des appels système classiques - Avec
Io.Evented, elle s’exécute via un backend asynchrone - Dans les deux cas,
writeAll()garantit que l’opération est terminée au moment de son appel
- Avec
- Le même code fonctionne de la même manière dans les contextes synchrone et asynchrone
- Les auteurs de bibliothèques n’ont pas à se soucier du mode d’exécution
Exécution parallèle et async() / concurrent()
- La fonction
async()demande une exécution asynchrone, mais avecIo.Threaded, elle peut s’exécuter immédiatement- Avec
Io.Evented, il est possible de sauvegarder deux fichiers en parallèle avec une exécution réellement asynchrone
- Avec
- La fonction
concurrent()est utilisée lorsque une exécution réellement parallèle est nécessaireIo.Threadedutilise un pool de threadsIo.Eventedfonctionne de la même manière queasync()
- Un mauvais choix de fonction (
asyncau lieu deconcurrent) est considéré comme un bug et ne peut pas être empêché au niveau du langage
Style de code et intégration au langage
- Sans syntaxe dédiée à l’asynchrone, le style de code Zig standard est conservé
- Les structures de contrôle existantes comme
try,defersont utilisées telles quelles - Andrew Kelley a indiqué que « cela se lit comme du code Zig standard »
- Les structures de contrôle existantes comme
- Un exemple de résolution DNS asynchrone est présenté
- Contrairement à
getaddrinfo(), il ne renvoie que la première réponse réussie et annule les autres requêtes
- Contrairement à
Planification future et état d’avancement
Io.Eventedest encore en phase expérimentale, avec un support incomplet sur certains systèmes d’exploitation- Une implémentation
Iocompatible WebAssembly est prévue, et un travail fonctionnel correspondant reste à effectuer - Il existe 24 tâches de suivi liées à
Io, dont la majorité sont encore incomplètes - Zig n’étant pas encore en version 1.0, l’I/O asynchrone et la génération de code natif restent des chantiers majeurs
- Ce modèle devrait permettre de réduire la fréquence de réécriture du code causée par des changements dans l’interface I/O
Synthèse des discussions communautaires
- Plusieurs commentaires jugent l’approche de Zig plus simple et plus flexible que le modèle
async/awaitde Rust- Rust devient plus complexe en combinant plusieurs executors
- Zig permet la coexistence de plusieurs executors grâce à l’interface
Io
- Certains soulignent que le code peut devenir un peu verbeux
- Mais une API explicite améliore la sécurité, les performances et le contrôle des tests
- Les discussions techniques ont aussi abordé la différence entre exécution asynchrone et exécution thread, ainsi que les implémentations de coroutines stackful et stackless
- L’
Iode Zig est implémenté sous forme d’extension de la bibliothèque standard, sans traitement spécial au niveau du langage- Une implémentation de coroutine stackless est prévue à l’avenir
Conclusion
- Le nouveau modèle asynchrone de Zig vise à concilier la simplicité du langage et les performances I/O élevées
- En résolvant le problème de la coloration de fonctions, en intégrant code synchrone et asynchrone, et grâce à une structure de contrôle explicite, il est considéré comme une étape clé vers la stabilisation de Zig 1.0
1 commentaires
Commentaires sur Hacker News
Dans l’ensemble, cet article est exact et bien documenté.
Il y a toutefois quelques petites corrections.
Dans une instance
Io.Threaded,async()ne fonctionne pas réellement de manière asynchrone et s’exécute immédiatement. En revanche,std.Io.Threadeddistribue par défaut les tâches asynchrones via un pool de threads.En revanche, si l’initialisation se fait avec
init_single_threaded, alors le comportement correspond bien à celui décrit dans l’article.Autre point : il existait auparavant une fonction appelée
asyncConcurrent(), mais elle a simplement été renommée enconcurrent()À l’avenir, pour envoyer des retours, vous pouvez écrire à lwn@lwn.net.
Merci pour les suggestions de correction et pour le travail autour de Zig
Je me demande quel type de bug peut apparaître si on utilise par erreur
asyncConcurrent()là où il faudraitasync().Selon le modèle d’IO, est-ce que cela peut aller jusqu’à de l’UB (comportement indéfini), ou s’agit-il simplement d’une erreur logique ?
concurrent()est aussi d’améliorer la lisibilité et l’expressivité du code, en montrant clairement : « ce code doit impérativement s’exécuter en parallèle »Cette conception me paraît plutôt raisonnable.
En revanche, l’explication de Zig est confuse.
Ils insistent sur le fait qu’ils auraient résolu le problème du function coloring, mais en pratique ils n’ont fait que pousser l’IO dans une sorte d’effect type.
L’appelant doit toujours conserver un token, ce qui reste une forme de coloration.
Cela me semble similaire à l’approche de Go pour l’asynchrone
L’ancien modèle async-await de Zig avait déjà résolu le problème du coloring.
Le compilateur générait automatiquement une version synchrone ou asynchrone selon le contexte d’appel
Zig résout cela via l’injection de dépendances, ce qui est largement suffisant en pratique.
La complexité des appels async est inévitable, mais c’est le prix à payer pour un contrôle fin
On peut déclarer une variable IO globale et l’utiliser partout, même si ce n’est évidemment pas recommandé pour écrire des bibliothèques.
Si l’on reprend les cinq critères du problème de function coloring décrits dans What color is your function?, l’approche de Zig a de fortes chances de ne pas satisfaire certaines conditions, notamment les 4 et 5
Mais cette approche peut provoquer des problèmes comme des deadlocks.
Certaines parties du code ne sont pas thread-safe, et dans ce cas le coloring peut au contraire être utile
Cette conception ressemble beaucoup à l’async de Scala.
En Scala, le contexte d’exécution est passé sous forme de paramètre implicite, alors que Zig le reçoit explicitement.
En pratique, cela n’apportait pas grand-chose de mieux que l’usage direct de threads et de files, et la gestion des contextes d’exécution provoquait des comportements complexes et imprévisibles.
L’équipe Zig semble penser que cette approche est nouvelle, probablement faute d’expérience avec Scala
La JVM contourne cela avec les threads virtuels, mais un langage bas niveau aura du mal à atteindre la même efficacité.
Un langage comme Zig a donc besoin d’une autre solution de passage à l’échelle
Dans l’ancien système async/await de Zig, les fonctions pouvaient être suspendues et reprises.
J’aurais aimé expérimenter cette capacité pour le développement OS, afin d’implémenter une suspension/reprise de frame pilotée par interruption matérielle.
Avec le nouveau système d’IO, j’ai l’impression qu’il faudra l’implémenter soi-même, ce qui est dommage
@asyncSuspendet@asyncResume.Le nouvel
Ioest une abstraction commune aux modèles synchrone, threadé et événementiel, donc il n’inclut pas de mécanisme de suspensionÀ voir l’actuel prototype Io.Evented, cela pourrait aussi être pris en charge par une bibliothèque tierce fondée sur des coroutines sans pile
Dans l’exemple, il est dit que le travail est terminé lorsque
writeAll()retourne,mais comme les implémentations d’IO peuvent varier, l’achèvement ne devrait en réalité être garanti qu’au moment où le defer commence.
Sinon, il faut suivre la relation de dépendance entre
createFileetwriteAll.Dans ce cas, cela finit par ressembler à un simple appel bloquant.
Par ailleurs, la raison pour laquelle cette interface s’appelle IO n’est pas très claire.
En réalité, elle se rapproche davantage d’une abstraction « exécuter dans un autre contexte »
Documentation associée : std.Io
L’exemple suivant est intéressant
En Rust comme en Python, une coroutine n’avance pas tant qu’elle n’est pas await.
En revanche, si dans l’exemple de Zig
io.asyncprogresse de lui-même, cela ressemble davantage à une création de tâche.C’est un choix de conception valide, mais ce n’est pas celui qu’ont retenu les autres langages
asyncs’exécute sur le thread appelant jusqu’au premieryield.await(io).Le fait qu’elle démarre immédiatement ou soit placée dans la file du pool de threads dépend de l’implémentation du runtime Io
await.Avec un IO événementiel, les deux tâches peuvent s’exécuter de façon entrelacée ; avec un IO threadé, elles peuvent avancer en arrière-plan.
Il n’y a donc pas de « tâche qui s’exécute en douce quelque part »
En utilisant Go au quotidien, j’ai l’impression que l’
Iode Zig corrige plusieurs de ses faiblesses.Je me demande toutefois si Zig a une notion de channel.
Dans Go, j’ai toujours trouvé dommage que le mot-clé select ne puisse pas s’appliquer aux sockets
Les channels de Go ont un surcoût de plusieurs dizaines de cycles, ce qui les rend peu efficaces pour de petites opérations d’IO.
En revanche, ils sont utiles pour les transferts de données de grande taille ou la synchronisation many-to-many
std.Io.Queue.On peut aussi implémenter quelque chose de proche de
select, même si c’est moins ergonomique sur le plan syntaxique.En contrepartie, cela peut fonctionner avec différents runtimes d’IO sans GC
L’approche « sans couleur » de Zig me paraît bien meilleure
Les goroutines ne sont que des green threads, et les channels de simples files thread-safe ; Zig fournit déjà tout cela dans sa bibliothèque standard
La version async de
Iodans Zig semble très proche de l’approche de Go.La différence, c’est que dans Go, les appels aux bibliothèques C entraînent un coût d’allocation de pile important, et les syscalls directs posent des problèmes de portabilité entre plateformes.
Zig semble avoir rendu cela configurable, afin de permettre différents compromis sans modifier le code
Le nouvel IO async est excellent sur des exemples simples, mais il pourrait montrer ses limites pour des IO complexes de niveau serveur.
J’ai ouvert une issue à ce sujet sur GitHub
Le problème central est que les concepteurs de langages ou de bibliothèques doivent fournir un moyen de relier différents contextes d’exécution (sync/async).
Pour cela, il faut encapsuler les contextes sous forme de FSM (machines à états finis) et fournir un canal de communication entre eux
Article connexe : Function colors represent different execution contexts