2 Punkte von GN⁺ 2025-10-28 | 1 Kommentare | Auf WhatsApp teilen
  • Der Autor arbeitete beim Neuschreiben des AcoustID-Index in Zig und versuchte ausgehend von den Grenzen der Netzwerkprogrammierung einen neuen Ansatz
  • Um das aus C++ und Go bekannte Modell für asynchrones I/O und Nebenläufigkeit auch in Zig umzusetzen, entschied er sich zur Entwicklung einer eigenen Bibliothek
  • Das Ergebnis ist die Zio-Bibliothek, die ein Go-ähnliches Nebenläufigkeitsmodell an Zig angepasst implementiert und es ermöglicht, asynchronen Code ohne Callbacks so zu schreiben, als wäre er synchron
  • Zio unterstützt asynchrones Netzwerk- und Datei-I/O, Channels, Synchronisationsprimitiven, Signalüberwachung und zeigt im Single-Thread-Modus eine höhere Leistung als Go oder Rusts Tokio
  • Das Projekt zeigt die Möglichkeit, System-Level-Performance von Zig mit modernen Nebenläufigkeitsmodellen zu verbinden, und gilt als wichtiger Wendepunkt für die Erweiterung des Zig-Ökosystems

Zig und die anfängliche Motivation

  • Der Autor hatte Zig, das ursprünglich als Low-Level-Sprache für Audiosoftware entworfen wurde, schon länger beobachtet, sah aber zunächst keinen konkreten Bedarf
    • Interesse entstand, nachdem Zig-Schöpfer Andrew Kelley den Chromaprint-Algorithmus des Autors in Zig neu implementiert hatte
  • Das Rewrite-Projekt für den Inverted Index von AcoustID nutzte er als Gelegenheit, Zig zu lernen, und erreichte am Ende eine schnellere und besser skalierende Implementierung als in C++
  • Beim Hinzufügen der Server-Schnittstelle stieß er jedoch auf das Problem der fehlenden Unterstützung für asynchrones Networking

Bisherige Ansätze und ihre Grenzen

  • In der früheren C++-Version wurde das Qt-Framework für asynchrones I/O verwendet; es war callback-basiert, aber dank des umfangreichen Supports praktikabel
  • In späteren Prototypen nutzte er die Bequemlichkeit von Networking und Nebenläufigkeit in Go, doch in Zig fehlte eine Abstraktion auf vergleichbarem Niveau
  • Um in Zig einen TCP-Server und eine Cluster-Schicht zu implementieren, hätten ineffizienterweise viele Threads erzeugt werden müssen
    • Um das zu lösen, schrieb er selbst einen Zig-Client für das Messaging-System NATS (nats.zig) und erforschte dabei Zigs Networking-Funktionen im Detail

Die Entstehung der Zio-Bibliothek

  • Auf Basis dieser Erfahrungen veröffentlichte er Zio: eine Bibliothek für asynchrones I/O und Nebenläufigkeit in Zig
  • Ziel von Zio ist das Schreiben asynchronen Codes ohne Callbacks; intern arbeitet asynchrones I/O, nach außen wirkt die Struktur jedoch wie synchroner Code
  • Es implementiert ein Go-ähnliches Nebenläufigkeitsmodell in einer an Zig angepassten, eingeschränkten Form
    • Zio-Tasks sind stackful Coroutines mit Stacks fester Größe
    • Beim Aufruf von stream.read() wird der I/O-Vorgang im Hintergrund ausgeführt; nach Abschluss wird der Task fortgesetzt und liefert das Ergebnis zurück
  • Dieser Ansatz vereinfacht zugleich das Zustandsmanagement und verbessert die Lesbarkeit des Codes

Funktionsumfang und Runtime-Struktur

  • Zio unterstützt vollständiges asynchrones Netzwerk- und Datei-I/O, Synchronisationsprimitiven (Mutexe, Condition Variables usw.), Go-ähnliche Channels sowie Überwachung von OS-Signalen
  • Tasks können im Single-Thread- oder Multi-Thread-Modus ausgeführt werden
    • Im Multi-Thread-Modus können Tasks zwischen Threads verschoben werden, was Latenzen senkt und den Lastausgleich verbessert
  • Durch die Implementierung der standardmäßigen Reader/Writer-Interfaces ist die Kompatibilität mit externen Bibliotheken gewährleistet

Leistung und Vergleich

  • Der Autor hat noch keine offiziellen Benchmarks veröffentlicht, erwähnt jedoch, dass er im Single-Thread-Modus eine höhere Leistung als Go und Rusts Tokio festgestellt habe
  • Die Kosten für Context Switching liegen auf dem Niveau eines Funktionsaufrufs und machen Wechsel praktisch nahezu kostenlos
  • Der Multi-Thread-Modus ist noch nicht so robust wie Go oder Tokio, zeigt aber ähnliche oder leicht bessere Leistung
    • Mit einer späteren Ergänzung von Fairness könnte die Leistung etwas sinken

Beispielcode und Nutzung

  • Die Dokumentation enthält Beispielcode für einen auf Zio basierenden HTTP-Server
    • Mit zio.net.Stream werden Verbindungen angenommen, und jede Verbindung wird in einem separaten Task verarbeitet
    • zio.Runtime verwaltet die Task-Ausführung und das I/O-Scheduling
  • Diese Struktur ermöglicht es, asynchrones I/O wie synchronen Code zu schreiben, und sorgt für klare Ablaufsteuerung sowie kontrollierte Freigabe von Ressourcen

Ausblick und Bedeutung

  • Der Autor sieht durch Zio bestätigt, dass Zig sich über eine bloße Sprache für performanten System-Code hinaus zu einer vollwertigen Sprache für die Entwicklung von Netzwerkapplikationen entwickeln kann
  • Als nächste Schritte plant er, den NATS-Client auf Basis von Zio neu zu schreiben und HTTP-Client-/Server-Bibliotheken auf Zio-Basis zu entwickeln
  • Das Projekt treibt den Ausbau der Networking- und Nebenläufigkeits-Infrastruktur im Zig-Ökosystem voran und wird als Versuch bewertet, ein modernes Runtime-Modell auf dem Niveau von Go oder Rust aufzubauen

1 Kommentare

 
GN⁺ 2025-10-28
Hacker-News-Kommentare
  • Es heißt zwar, Kontextwechsel seien auf Funktionsaufruf-Ebene fast kostenlos, aber in der Praxis gibt es subtile Kosten, etwa wenn der Branch Predictor aus dem Tritt gerät
    Es ist unklar, ob Zigs Async-Design Hardware-Call/Return-Paare nutzt oder in indirekte Sprünge übersetzt wird
    Für einen sauberen Benchmark müsste man die Gesamtlaufzeit eines Programms mit ständigem Wechsel zwischen zwei Tasks mit der eines vollständig synchronen Programms vergleichen. Das ist ziemlich knifflig
    • Bei stacklosen Coroutines kann man den Call/Ret-Mismatch-Penalty größtenteils vermeiden, wenn man am unteren Ende des Call-Stacks ständig zwischen zwei Tasks wechselt und der Stack-Switching-Code inline ist
      Wenn man den Compiler kontrollieren kann, wäre es auch möglich, Call/Ret im I/O-Code durch explizite Sprünge zu ersetzen
      Langfristig wäre es wünschenswert, wenn CPUs einen Meta-Predictor einführen würden, damit stackful Coroutines besser vorhersehbar sind
    • In Zig ist Async auf Sprachebene derzeit verschwunden, und der OP hat Task-Switching im User Space direkt selbst implementiert
    • Bei einem einfachen Ping-Pong-Test zwischen Coroutines habe ich einmal im Vergleich zu anderen Lösungen kaum glaubwürdige Zahlen erhalten
    • Zig soll bald ein neues Async bekommen, daher warte ich noch, bevor ich tiefer einsteige
      Zugehöriger Artikel: Zig new async I/O
  • Stackful Coroutines sind sinnvoll, wenn genug RAM vorhanden ist
    Ich nutze Zig in einer Embedded-Umgebung (ARM Cortex-M4, 256 KB RAM) und verwende es, um bei der Interoperabilität mit C Speichersicherheit zu gewährleisten
    Ich bevorzuge eher farbiges Async wie in Rust. Dieses magische Gefühl, dass es wie synchroner Code aussieht, ist schön, aber in großen Codebasen wird es schwierig zu erkennen, welche Funktionen blockierend sind
    • Tatsächlich ist aller synchroner Code eine von Software erzeugte Illusion
      Die CPU blockiert bei I/O nicht wirklich, und OS-Threads selbst sind stackful Coroutines, die vom OS implementiert werden
      Auf Sprachebene lässt sich diese Illusion nur effizienter umsetzen, aber im Wesen ist es dasselbe
    • Das neue Zig-I/O soll eine farbige Struktur bekommen, und zwar eleganter als bei Rust
      Die Farbe einer Funktion wird dadurch bestimmt, ob sie I/O ausführt, und beim Aufruf wird explizit angegeben, ob es async ist
      Zig will außerdem die für einen Funktionsaufruf benötigte Stack-Größe berechnen können, wodurch sich das RAM-Verschwendungsproblem stackfuler Coroutines verringern ließe
    • Genau deshalb will Zig I/O explizit ausdrücken, damit man verfolgen kann, welche Funktionen blockierend sind
  • Manche meinen, für einen Einstieg in Zig sei es derzeit noch zu früh. Das I/O-Modell ändere sich stark, und es wirke so, als dauere das noch einige Jahre
    • Ich habe Zig 2020 aus einem ähnlichen Grund ebenfalls verlassen.
      Das Projekt ist aber weiterhin sehr aktiv, und ich sehe es positiv, dass korrektes Design vor schnellen Releases priorisiert wird
      Im Moment nutze ich Go oder C und warte auf 1.0
    • Ein paar Jahre gehen schnell vorbei. Zig ist bereits eine durchaus brauchbare Sprache. Wer es nutzen will, nutzt es, und wer nicht, eben nicht
    • Tatsächlich ist es ein schlechter Zeitpunkt. Für 0.16 sind große I/O-Änderungen geplant, und nicht einmal der Autor selbst nutzt die neuesten Features bislang
      Ich werde für I/O-lastige Arbeit ebenfalls auf 0.16 warten
    • Wenn es aber um I/O-bezogene Arbeit geht, dürfte die Nutzung der gepufferten Reader-/Writer-Interfaces in Zig 0.15 keine großen Veränderungen mit sich bringen
    • Ich sehe das eher anders. Nicht die Sprache Zig verändert sich rasant, sondern es wird lediglich eine neue leistungsstarke std.Io-API ergänzt
      Bestehender Code funktioniert weiter, und die neue API ist ergonomischer und performanter
      Ich habe mein bestehendes Projekt ebenfalls auf die neue Reader/Writer-API umgestellt, und der Code ist dadurch viel sauberer geworden
  • Warum callback-basiertes Async zum Standard geworden ist, ist mir immer noch ein Rätsel
    Ein Ansatz wie libtask wirkt deutlich sauberer
    Auch Rust hat callback-basiertes Async übernommen, und ich verstehe nicht wirklich warum
    Siehe: libtask
    • Stacklose Coroutines lassen sich innerhalb der Sprache implementieren und haben den Vorteil einer vorhersehbaren Interaktion mit bestehenden Features
      Wenn man jedoch den Stack direkt manipuliert, kann es zu Konflikten mit Exception Handling, GC, Debuggern usw. kommen
      Auch auf LLVM-Ebene sind solche Änderungen schwer zusammenzuführen, daher gibt es aus Sicht von Sprachdesignern viele praktische Einschränkungen
    • Microsoft kam in Untersuchungen für den C++-Standard zu dem Ergebnis, dass stacklose Coroutines deutlich weniger Speicher-Overhead haben und beim Executor-Design mehr Freiheitsgrade bieten
    • Ein Nachteil von Ansätzen wie zio oder libtask ist, dass man die Stack-Größe selbst abschätzen muss
      Ist sie zu klein, gibt es einen Overflow, ist sie zu groß, wird Speicher verschwendet
      Zudem unterscheidet sich die benötigte Stack-Größe je nach Plattform, was Portabilitätsprobleme verursacht
      Wenn Zig-Issue #157 gelöst wird, könnte dieser Ansatz besser werden
    • Bei libtask etwa ist die Thread-Stack-Größe unklar und viel größer als ein gewöhnlicher Async-Status
    • Rusts Async ist nicht callback-, sondern polling-basiert
      Es gibt also drei Wege, Async zu implementieren
      1. callback-basiert (Node.js, Swift)
      2. stackful-basiert (Go, libtask)
      3. polling-basiert (Rust)
        Rust wird in eine statische State Machine transformiert, die dann von der Runtime gepollt wird
        Stackful-Ansätze verschwenden viel Speicher, und die Verwaltung der Stack-Größe ist schwierig
        Rust hat zur Vermeidung dessen eine stacklose Struktur gewählt, und Zig will künftig beide Wege zur Auswahl stellen
        Siehe: zio-Coroutine-Code
  • Ein TCP-Read kann auch einen Monat lang blockieren; ich frage mich, wie das I/O-Timeout-Interface dafür aussieht
    • Bei TCP-Sockets kann man mit setsockopt Lese-/Schreib-Timeouts setzen
      Zig stellt eine POSIX-API-Schicht bereit
      Siehe: setsockopt-Dokumentation
    • Aktuell erkennt Zigs std.Io.Reader keine Timeouts
      Ich denke über eine Struktur nach, die ähnlich wie asyncio.timeout in Python funktioniert
      Beispielcode:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • Die meisten Async-Frameworks übersehen Timeouts und Abbruch
      Tatsächlich ist das der schwierigste Teil
  • In Scala gibt es bereits eine Concurrency-Bibliothek namens ZIO
    Siehe: zio.dev
  • Ich war kürzlich von Rusts Tokio beeindruckt; wenn sich in Zig Go-artige Concurrency ohne GC umsetzen ließe, würde ich es unbedingt ausprobieren wollen
    • Go kann dank GC Tricks wie unendlich skalierbare Stacks verwenden
      Trotzdem fand ich beeindruckend, dass Zig als Low-Level-Sprache dennoch High-Level-APIs sauber ausdrücken kann
  • Auf Zig aufmerksam geworden bin ich zuerst über die Bun-Website. Es entwickelt sich derzeit wirklich schnell
  • In einer früheren C++-Version habe ich asynchrones I/O mit Qt implementiert, diesmal bin ich aber auf Go umgestiegen
    Sowohl für Zig als auch für Go gibt es neue Qt-Bindings
    • Go: miqt
    • Zig: libqt6zig
      Ich hätte gern Bindings für Rust. cxx-qt ist das einzige Projekt, das noch gepflegt wird, aber ich möchte weder QML noch CMake verwenden. Ich möchte Qt nur mit Rust + Cargo nutzen
  • Auch in Scala gibt es bereits das bekannte Framework ZIO, was wieder zeigt, wie schwierig Namensgebung ist