4 points par GN⁺ 2025-12-04 | 1 commentaires | Partager sur WhatsApp
  • Le langage Zig introduit un nouveau modèle basé sur l’interface Io pour 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.Threaded et Io.Evented
  • Io.Threaded exécute par défaut de manière synchrone, tandis que Io.Evented exécute en mode asynchrone basé sur une boucle d’événements
  • Les développeurs peuvent contrôler l’exécution parallèle via async() et concurrent(), 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 Io en 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

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écessaire
    • Io.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
  • 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 avec Io.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
  • La fonction concurrent() est utilisée lorsque une exécution réellement parallèle est nécessaire
    • Io.Threaded utilise un pool de threads
    • Io.Evented fonctionne de la même manière que async()
  • Un mauvais choix de fonction (async au lieu de concurrent) 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, defer sont utilisées telles quelles
    • Andrew Kelley a indiqué que « cela se lit comme du code Zig standard »
  • 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

Planification future et état d’avancement

  • Io.Evented est encore en phase expérimentale, avec un support incomplet sur certains systèmes d’exploitation
  • Une implémentation Io compatible 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/await de 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’Io de 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

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

    • C’est Daroc. J’ai appliqué à l’article deux corrections en tenant compte de ce retour.
      À 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
    • J’ai une question pour Andrew.
      Je me demande quel type de bug peut apparaître si on utilise par erreur asyncConcurrent() là où il faudrait async().
      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 ?
    • L’intérêt de 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

    • Si le simple fait d’appeler une fonction avec des arguments différents suffit à la « colorer », alors toutes les fonctions sont colorées et le concept perd tout son sens ;)
      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
    • En réalité, le cœur du problème du function coloring, c’est la duplication des chemins de code synchrones et asynchrones.
      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
    • L’IO de Zig n’est pas un effect type contagieux.
      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
    • En pratique, Zig semble colorer tout en async, puis laisser le choix d’utiliser ou non des threads workers.
      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
    • Vu de loin, en tant que développeur Haskell, on a l’impression que Zig a implémenté une monade IO sans support du langage
  • 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

    • Si l’on utilise directement les threads de l’OS, on finit par buter sur des limites de scalabilité conformément à la loi de Little.
      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
    • À titre de référence, l’API ExecutionContext de Scala aide à mieux comprendre les concepts concernés
  • 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

    • Il existe des built-ins bas niveau : @asyncSuspend et @asyncResume.
      Le nouvel Io est une abstraction commune aux modèles synchrone, threadé et événementiel, donc il n’inclut pas de mécanisme de suspension
    • À terme, suspend/resume pourrait être implémenté comme fonction de bibliothèque standard en espace utilisateur.
      À 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
    • Je me demande aussi s’il est possible d’implémenter suspend/resume avec un seul pool de threads
    • Je vois mal ce que signifie implémenter des coroutines coopératives sous forme d’async préemptif
  • 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 createFile et writeAll.
    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

    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);
    

    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.async progresse 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

    • C# fonctionne de manière assez similaire. Une fonction async s’exécute sur le thread appelant jusqu’au premier yield
    • Dans Zig aussi, l’exécution n’est garantie qu’au moment de l’appel à .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
    • En pratique, l’exécution progresse bien au moment du 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 »
    • JavaScript fonctionne aussi ainsi
  • En utilisant Go au quotidien, j’ai l’impression que l’Io de 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

    • Il faut noter qu’envelopper toute l’IO dans des channels a un coût élevé.
      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
    • Zig propose un équivalent aux channels de Go avec 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
    • J’aimerais savoir si vous avez essayé le langage Odin. C’est un « better C » plus inspiré par Go que Zig
    • J’aime le fait que, contrairement à l’async/await de C#, cela n’impose pas de fonctions colorées.
      L’approche « sans couleur » de Zig me paraît bien meilleure
    • C’est problématique de croire que le modèle de concurrence de Go aurait quelque chose de spécial.
      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 Io dans 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