- 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
Hacker-News-Kommentare
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
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
Zugehöriger Artikel: Zig new async I/O
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
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
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
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
Ich werde für I/O-lastige Arbeit ebenfalls auf 0.16 warten
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
Ein Ansatz wie libtask wirkt deutlich sauberer
Auch Rust hat callback-basiertes Async übernommen, und ich verstehe nicht wirklich warum
Siehe: libtask
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
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
Es gibt also drei Wege, Async zu implementieren
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
setsockoptLese-/Schreib-Timeouts setzenZig stellt eine POSIX-API-Schicht bereit
Siehe: setsockopt-Dokumentation
Ich denke über eine Struktur nach, die ähnlich wie
asyncio.timeoutin Python funktioniertBeispielcode:
Tatsächlich ist das der schwierigste Teil
Siehe: zio.dev
Trotzdem fand ich beeindruckend, dass Zig als Low-Level-Sprache dennoch High-Level-APIs sauber ausdrücken kann
Sowohl für Zig als auch für Go gibt es neue Qt-Bindings
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