4 Punkte von GN⁺ 2025-12-04 | 1 Kommentare | Auf WhatsApp teilen
  • Die Zig-Sprache führt ein neues Modell auf Basis der Io-Schnittstelle ein, um die Komplexität des bisherigen asynchronen I/O-Designs zu reduzieren
  • Dieses Modell behält eine einheitliche Funktionsstruktur ohne Unterscheidung zwischen synchronem und asynchronem Code bei und stellt zwei Implementierungen bereit: Io.Threaded und Io.Evented
  • Io.Threaded führt standardmäßig synchrone Ausführung aus, während Io.Evented eventbasierte asynchrone Ausführung durchführt
  • Entwickler können mit den Funktionen async() und concurrent() die Parallelausführung steuern; ohne Codeänderung ist eine Leistungsoptimierung möglich
  • Dieser Ansatz adressiert das Problem des Function Coloring und zielt darauf ab, Zigs Einfachheit und Kontrollierbarkeit zu bewahren und zugleich asynchrone Leistung sicherzustellen

Änderungen im asynchronen Design von Zig

  • Zig hat nach einer neuen Herangehensweise gesucht, da das frühere asynchrone Design nicht gut zur Minimalismus-Philosophie der Sprache passte
    • Das alte Design hatte eine niedrige Integration mit anderen Sprachfeatures
    • Das neue Modell kann synchrones und asynchrones I/O mit derselben Code-Struktur handhaben
  • Das neue Modell ist auf einem generischen Io-Interface aufgebaut
    • Alle I/O-Funktionen führen ihre Ausführung über eine als Parameter übergebene Io-Instanz aus
    • Ähnlich wie bei der Allocator-Schnittstelle ist es möglich, I/O auf ähnliche Weise wie Speicherzuweisung zu steuern

Struktur des Io-Interfaces

  • Die Standardbibliothek enthält zwei Standardimplementierungen
    • Io.Threaded: standardmäßig synchrone Ausführung, optional parallele Verarbeitung mit Threads
    • Io.Evented: eventloopbasierte asynchrone Ausführung (unter anderem mit io_uring, kqueue)
  • Anwender können eigene neue Io-Implementierungen schreiben und so die Ausführungsmethode sehr fein steuern

Codebeispiel und Funktionsweise

  • Die Beispiel-Funktion saveFile() erstellt, schreibt und schließt eine Datei
    • Mit Io.Threaded läuft sie über normale Systemaufrufe
    • Mit Io.Evented läuft sie über ein asynchrones Backend
    • In beiden Fällen ist die Fertigstellung beim Aufruf von writeAll() garantiert
  • Der gleiche Code arbeitet in synchronen wie asynchronen Umgebungen gleich
    • Bibliotheksautor:innen müssen sich nicht um die Ausführungsart kümmern

Parallelausführung mit async() / concurrent()

  • Die Funktion async() fordert eine asynchrone Ausführung an, kann aber bei Io.Threaded sofort ausgeführt werden
    • In Io.Evented erfolgt echte asynchrone Ausführung, sodass beide Dateien parallel gespeichert werden können
  • Die Funktion concurrent() wird verwendet, wenn echte Parallelität nötig ist
    • Io.Threaded nutzt dabei den Thread-Pool
    • Io.Evented behandelt sie wie async()
  • Eine falsche Funktionsauswahl (async anstelle von concurrent) gilt als Bug; sie kann sprachlich nicht verhindert werden

Code-Stil und Sprachintegration

  • Ohne asynchrone Spezialsyntax bleibt der normale Zig-Code-Stil erhalten
    • Bestehende Kontrollflusskonstrukte wie try und defer werden unverändert verwendet
    • Andrew Kelley sagt, dass es sich wie normaler Zig-Code anfühlt
  • Als Beispiel wird eine asynchrone DNS-Auflösung gezeigt
    • Anders als bei getaddrinfo() wird nur die erste erfolgreiche Antwort zurückgegeben und die restlichen Anfragen werden abgebrochen

Ausblick und aktueller Stand

  • Io.Evented befindet sich noch in der experimentellen Phase; einige Betriebssysteme unterstützen es noch nicht
  • Eine WebAssembly-kompatible Io-Implementierung ist geplant und erfordert die Entwicklung entsprechender Features
  • Zu Io gibt es 24 Folgeaufgaben, von denen die meisten noch offen sind
  • Zig ist noch vor Version 1.0; asynchrone I/O und native Codegenerierung sind weiterhin die wichtigsten offenen Aufgaben
  • Mit diesem Entwurf wird erwartet, dass sich die Notwendigkeit zur Code-Überarbeitung aufgrund von Änderungen an der I/O-Schnittstelle verringert

Zusammenfassung der Community-Diskussion

  • Mehrere Kommentare bewerten Zigs Ansatz als einfacher und flexibler als Rusts async/await-Modell
    • In Rust ist die Komplexität hoch, wenn mehrere Executor gleichzeitig eingesetzt werden
    • Mit dem Io-Interface eröffnet Zig die Möglichkeit, mehrere Executor parallel zu betreiben
  • Einige bemängeln, dass der Code etwas umfangreich werden kann
    • Durch das explizite API-Design verbessert sich jedoch die Kontrolle über Sicherheit, Leistung und Tests
  • Auch technische Diskussionen über den Unterschied zwischen asynchroner und Thread-Ausführung sowie über stackful vs. stackless Coroutine-Umsetzungen wurden geführt
  • Zig Io ist als Standardbibliotheks-Erweiterung implementiert, ohne Sprach-spezielle Sonderbehandlung
    • Künftige Erweiterung um stackless coroutine-Funktionen ist vorgesehen

Fazit

  • Das neue asynchrone Modell von Zig zielt darauf ab, sprachliche Einfachheit mit leistungsstarker I/O zu verbinden
  • Durch die Lösung des Function-Coloring-Problems, die Integration von synchronem und asynchronem Code und eine explizite Kontrollstruktur wird es als Schlüssel-Schritt zur Stabilisierung von Zig 1.0 bewertet

1 Kommentare

 
GN⁺ 2025-12-04
Hacker-News-Kommentar
  • Insgesamt ist der Artikel korrekt und gut recherchiert.
    Es gibt nur ein paar kleine Korrekturen.
    Bei Io.Threaded-Instanzen arbeitet async() tatsächlich nicht asynchron, sondern wird sofort ausgeführt. Allerdings verwendet std.Io.Threaded standardmäßig einen Thread-Pool, um asynchrone Aufgaben zu verteilen.
    Wird es jedoch mit init_single_threaded initialisiert, verhält es sich wie im Artikel beschrieben.
    Außerdem gab es früher eine Funktion namens asyncConcurrent(), die inzwischen einfach in concurrent() umbenannt wurde

    • Hier ist Daroc. Ich habe dieses Feedback berücksichtigt und zwei Korrekturen im Artikel vorgenommen.
      Wenn du künftig Feedback geben möchtest, kannst du eine Mail an lwn@lwn.net schicken.
      Danke für die Korrekturvorschläge und für die Arbeit rund um Zig
    • Ich habe eine Frage an Andrew.
      Mich würde interessieren, welche Bugs entstehen, wenn man versehentlich asyncConcurrent() dort verwendet, wo async() nötig wäre.
      Kann das je nach IO-Modell zu UB (undefiniertem Verhalten) führen, oder ist es nur ein logischer Fehler?
    • Das Gute an concurrent() ist, dass es die Lesbarkeit und Ausdruckskraft des Codes erhöht, weil klar wird: „Dieser Code muss zwingend parallel laufen.“
  • Ich halte dieses Design für ziemlich vernünftig.
    Die Erklärung von Zig ist allerdings verwirrend.
    Es wird betont, dass das Problem des Function Coloring gelöst sei, tatsächlich wurde IO aber im Wesentlichen nur in einen Effect Type ausgelagert.
    Der Aufrufer muss dabei weiterhin ein Token mitführen, also ist es immer noch eine Art Coloring.
    Das erscheint mir ähnlich zur Art, wie Go mit Asynchronität umgeht

    • Wenn schon ein anderer Aufruf mit anderen Argumenten eine „eingefärbte Funktion“ macht, dann sind letztlich alle Funktionen eingefärbt und der Begriff verliert seine Bedeutung ;)
      Zigs früheres Async-Await-Modell hatte das Coloring-Problem bereits gelöst.
      Der Compiler erzeugte je nach Aufrufkontext automatisch synchrone/asynchrone Versionen
    • Tatsächlich ist das Kernproblem von Function Coloring die Duplizierung synchroner/asynchroner Codepfade.
      Zig löst das in der Praxis ausreichend über Dependency Injection.
      Die Komplexität von Async-Aufrufen lässt sich nicht vermeiden, aber für präzise Kontrolle ist das wohl unvermeidlich
    • Zigs io ist kein ansteckender Effect Type.
      Man kann eine globale io-Variable deklarieren und sie überall verwenden, auch wenn das beim Schreiben von Bibliotheken natürlich nicht empfohlen wird.
      Wenn man den Artikel What color is your function? mit seinen fünf Bedingungen für Function Coloring betrachtet, erfüllt Zigs Ansatz vermutlich einige davon nicht, besonders 4 und 5
    • Im Grunde scheint Zig alles mit async einzufärben und nur die Wahl zu lassen, ob Worker-Threads verwendet werden oder nicht.
      So ein Ansatz kann aber Probleme wie Deadlocks verursachen.
      Manche Teile von Code sind nicht thread-sicher, daher kann Coloring sogar hilfreich sein
    • Aus Sicht eines Haskell-Entwicklers sieht es so aus, als hätte Zig ohne Sprachunterstützung eine IO-Monade implementiert
  • Dieses Design wirkt Scala async sehr ähnlich.
    In Scala wird der Execution Context als impliziter Parameter weitergereicht, in Zig explizit.
    In der Praxis war das gegenüber der direkten Nutzung von Threads und Queues oft kaum besser, und das Management des Execution Contexts führte zu komplexem und unvorhersehbarem Verhalten.
    Das Zig-Team scheint diesen Ansatz für neu zu halten, vermutlich weil es mit Scala weniger Erfahrung hat

    • Wenn man OS-Threads direkt verwendet, stößt man gemäß Littles Gesetz an Skalierungsgrenzen.
      Die JVM löst das mit virtuellen Threads, aber Low-Level-Sprachen erreichen diese Effizienz nur schwer.
      Deshalb brauchen Sprachen wie Zig andere Lösungen für Skalierbarkeit
    • Zum Verständnis der Konzepte lohnt sich ein Blick auf die ExecutionContext API von Scala
  • Im früheren Async/Await-System von Zig konnte man Funktionen suspendieren und fortsetzen.
    Damit wollte ich bei der OS-Entwicklung einmal Frame-Suspend/Resume auf Basis von Geräte-Interrupts umsetzen.
    Im neuen io-System muss man das offenbar selbst implementieren, was ich schade finde

    • Es gibt die Low-Level-Built-ins @asyncSuspend und @asyncResume.
      Das neue Io ist eine gemeinsame Abstraktion für synchrones, thread-basiertes und eventbasiertes Arbeiten und enthält daher keinen Suspend-Mechanismus
    • Am Ende könnte Suspend/Resume als Standardbibliotheksfunktion im Userspace umgesetzt werden.
      Wenn man sich den aktuellen Io.Evented-Prototyp ansieht, könnte das auf Basis von stackless coroutines auch von Drittanbieter-Bibliotheken behandelt werden
    • Ich frage mich auch, ob sich Suspend/Resume mit nur einem Thread-Pool umsetzen lässt
    • Ich bin mir außerdem nicht sicher, welchen Sinn es ergibt, kooperative Coroutines als präemptives Async zu implementieren
  • Im Beispielcode heißt es, dass die Arbeit abgeschlossen sei, wenn writeAll() zurückkehrt.
    Da es verschiedene IO-Implementierungen geben kann, müsste die Fertigstellung in Wirklichkeit schon beim Start von defer garantiert sein.
    Andernfalls müsste man die Abhängigkeit zwischen createFile und writeAll verfolgen.
    Dann wirkt das letztlich kaum anders als ein blockierender Aufruf.
    Außerdem ist unklar, warum dieses Interface IO heißt.
    Tatsächlich ist es eher eine Abstraktion für „in einem anderen Kontext ausführen“
    Relevante Dokumentation: std.Io

  • Das folgende Beispiel ist interessant

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

    In Rust oder Python laufen Coroutines nicht weiter, wenn sie nicht awaited werden.
    Im Zig-Beispiel wäre io.async, falls es selbstständig weiterläuft, eher mit Task-Erzeugung vergleichbar.
    Das ist ein gültiges Design, aber nicht der Weg, den andere Sprachen gewählt haben

    • In C# ist es ähnlich. Eine async-Funktion läuft im aufrufenden Thread bis zum ersten yield
    • Auch in Zig gilt: Erst mit .await(io) ist die Ausführung garantiert.
      Ob sofort ausgeführt oder in den Thread-Pool eingereiht wird, hängt von der Io-Runtime-Implementierung ab
    • Tatsächlich setzt die Ausführung beim await ein.
      Bei eventbasiertem io können die beiden Aufgaben verschachtelt (interleaved) ablaufen, bei threaded io auch im Hintergrund.
      Es gibt also keine „heimlich irgendwo laufenden Tasks“
    • JavaScript funktioniert ebenfalls auf diese Weise
  • Als jemand, der täglich Go nutzt, habe ich den Eindruck, dass Zigs Io mehrere Nachteile von Go korrigiert.
    Ich frage mich allerdings, ob Zig ein Konzept wie Channels hat.
    In Go gibt es zwar das Schlüsselwort select, aber dass es nicht mit Sockets verwendet werden kann, fand ich immer schade

    • Es wurde darauf hingewiesen, dass es teuer ist, sämtliches IO in Channels zu verpacken.
      Go-Channels haben einen Overhead von einigen Dutzend Zyklen und sind daher für kleinteiliges IO ineffizient.
      Für größere Datenbewegungen oder Many-to-Many-Synchronisation sind sie dagegen nützlich
    • Zig hat mit std.Io.Queue etwas, das den Go-Channels ähnelt.
      Auch ein select-artiges Konstrukt lässt sich ähnlich umsetzen, ist syntaktisch aber weniger ergonomisch.
      Dafür kann es ohne GC mit verschiedenen IO-Runtimes arbeiten
    • Mich würde interessieren, ob du schon einmal die Sprache Odin ausprobiert hast. Sie ist eher ein von Go als von Zig inspiriertes „better C“
    • Mir gefällt, dass Zig im Gegensatz zu C# async/await keine eingefärbten Funktionen erzwingt.
      Ich halte Zigs „colorless“-Ansatz für deutlich besser
    • Es ist problematisch zu glauben, Gos Concurrency-Modell sei etwas Besonderes.
      Goroutines sind nur Green Threads, Channels nur thread-sichere Queues, und Zig bietet beides bereits in der Standardbibliothek
  • Die asynchrone Io-Version von Zig wirkt dem Ansatz von Go fast völlig gleich.
    In Go sind jedoch bei Aufrufen von C-Bibliotheken die Kosten der Stack-Allokation hoch, und direkte Syscalls bringen Probleme bei der Plattformkompatibilität mit sich.
    Zig scheint das konfigurierbar gemacht zu haben, sodass sich verschiedene Trade-offs ohne Codeänderungen wählen lassen

  • Das neue Async-IO ist für einfache Beispiele hervorragend, könnte aber bei IO-Komplexität auf Server-Niveau an Grenzen stoßen.
    Ein dazugehöriges Issue habe ich auf GitHub eingestellt

  • Das Kernproblem ist, dass Sprach- oder Bibliotheksdesigner Mittel bereitstellen müssen, um verschiedene Ausführungskontexte (sync/async) miteinander zu verbinden.
    Dazu braucht man einen Ansatz, bei dem Kontexte in FSMs (endliche Zustandsautomaten) gekapselt werden und zwischen beiden Seiten Kommunikationskanäle bereitstehen
    Verwandter Artikel: Function colors represent different execution contexts