7 Punkte von GN⁺ 2025-05-18 | 4 Kommentare | Auf WhatsApp teilen
  • Der Vorschlag Explicit Resource Management führt eine neue Methode ein, um den Lebenszyklus von Ressourcen wie Datei-Handles und Netzwerkverbindungen klar zu steuern
  • Die Funktion ist ab Chromium 134 und V8 v13.8 verfügbar
  • Ergänzungen der Sprache
    • Die Einführung der Deklarationen using und await using sowie der Symbole Symbol.dispose und Symbol.asyncDispose stellt einen automatischen Cleanup-Mechanismus bereit
    • DisposableStack und AsyncDisposableStack gruppieren und geben mehrere Ressourcen sicher frei
    • SuppressedError verwaltet Fehler, die während des Cleanup auftreten, zusammen mit dem ursprünglichen Fehler
  • Dieser Ansatz verbessert Code-Sicherheit und Wartbarkeit erheblich und ist wirksam gegen Ressourcenlecks
  • Er vereinfacht das bestehende Muster mit try...finally und ermöglicht in großen, komplexen Ressourcenumgebungen eine zuverlässige Ressourcenverarbeitung

Überblick über den Vorschlag zum expliziten Ressourcenmanagement

  • Der Vorschlag Explicit Resource Management führt eine neue Methode ein, mit der sich Ressourcen wie Datei-Handles und Netzwerkverbindungen eindeutig erzeugen und freigeben lassen
  • Die wichtigsten Bestandteile sind wie folgt
    • Deklarationen using und await using: Ressourcen werden beim Verlassen des Scopes automatisch freigegeben
    • Symbole [Symbol.dispose]() und [Symbol.asyncDispose]() : Methoden zur Implementierung des Freigabe- bzw. Cleanup-Verhaltens
    • Globale Objekte DisposableStack und AsyncDisposableStack: gruppieren mehrere Ressourcen und verwalten sie effizient
    • SuppressedError: ein neuer Fehlertyp, der sowohl Fehler während der Ressourcenbereinigung als auch den ursprünglichen Fehler enthält
  • Diese Funktionen konzentrieren sich darauf, dass Entwickler Ressourcen fein granular verwalten und Leistung sowie Sicherheit des Codes verbessern können

Deklarationen using und await using

  • Die Deklaration using wird für synchrone Ressourcen verwendet, await using für asynchrone Ressourcen
  • Für deklarierte Ressourcen wird beim Verlassen des Scopes automatisch Symbol.dispose** bzw. **[Symbol.asyncDispose]() aufgerufen
  • So lassen sich Probleme mit Lecks bei synchronen und asynchronen Ressourcen reduzieren und konsistenter Freigabecode schreiben
  • Diese Schlüsselwörter können nur in Codeblöcken, for-Schleifen und Funktionsrümpfen verwendet werden, nicht auf der obersten Ebene
  • Beispiel
    • Wenn etwa ReadableStreamDefaultReader verwendet wird, muss zwingend reader.releaseLock() aufgerufen werden, damit der Stream wiederverwendet werden kann
    • Wird dieser Aufruf bei einem Fehler ausgelassen, kann der Stream dauerhaft gesperrt bleiben
  • Traditioneller Ansatz
    • Entwickler verwenden einen try...finally-Block, um sicherzustellen, dass die Sperre des Readers aufgehoben wird
    • Im finally-Block muss reader.releaseLock() geschrieben werden
  • Verbesserter Ansatz: Einführung von using
    • Es wird ein disposable Objekt (readerResource) mit Freigabelogik erzeugt
    • Mit dem Muster using readerResource = {...} erfolgt die Freigabe automatisch, sobald der Codeblock verlassen wird
    • Wenn Web-APIs künftig [Symbol.dispose] und [Symbol.asyncDispose] unterstützen, könnte eine automatische Verwaltung auch ohne separates Wrapper-Objekt möglich werden

DisposableStack und AsyncDisposableStack

  • DisposableStack und AsyncDisposableStack werden eingeführt, um mehrere Ressourcen effizient und sicher zu gruppieren
  • Ressourcen werden den jeweiligen Stacks hinzugefügt; wird der Stack selbst freigegeben, werden alle enthaltenen Ressourcen in umgekehrter Reihenfolge freigegeben
  • Das reduziert Risiken beim Umgang mit komplexen Ressourcengruppen mit Abhängigkeiten und vereinfacht den Code
  • Wichtige Methoden
    • use(value): Fügt oben auf dem Stack eine disposable Ressource hinzu
    • adopt(value, onDispose): Fügt eine nicht-disposable Ressource zusammen mit einem Freigabe-Callback hinzu
    • defer(onDispose): Fügt nur eine Freigabeaktion ohne Ressource hinzu
    • move(): Verschiebt alle Ressourcen des aktuellen Stacks in einen neuen Stack und ermöglicht so die Übergabe des Besitzes
    • dispose(), asyncDispose(): Gibt alle Ressourcen im Stack frei

Support-Status und möglicher Einsatzzeitpunkt

  • Das explizite Ressourcenmanagement ist ab Chromium 134 und V8 v13.8 verfügbar
  • Künftig wird eine breitere Kompatibilität mit verschiedenen Web-APIs erwartet

4 Kommentare

 
cichol 2025-05-18

await using data = await fn()
Das Wunder, dass await sowohl auf der linken als auch auf der rechten Seite steht

 
GN⁺ 2025-05-18
Hacker-News-Kommentare
  • Dieser Vorschlag vermittelt ein ähnliches Gefühl wie das Problem der „Färbung von Funktionen“. Die Unterscheidung zwischen synchronen und asynchronen Funktionen dringt immer weiter in alle Funktionen ein. Das sieht man zum Beispiel an Symbol.dispose und Symbol.asyncDispose sowie an DisposableStack und AsyncDisposableStack. Ich bin zufrieden damit, dass Java den Weg mit virtuellen Threads eingeschlagen hat. Ich denke, das ist eine Entscheidung, die dem JVM zwar mehr Komplexität hinzufügt, dafür aber die Last für Anwendungsentwickler, Bibliotheksautoren und Debugger verringert

    • Dem stimme ich nicht zu, denn wenn man Asynchronität versteckt, wird der Kontrollfluss im Code noch schwerer zu verstehen. Ich möchte auch wissen, ob Ressourcen asynchron freigegeben werden und ob das von externen Faktoren wie Netzwerkproblemen beeinflusst werden kann

    • Dieses heutige Phänomen in den meisten Sprachen, dass „man selbstverständlich allen Code asynchron schreibt“, nervt mich wirklich. Purescript ist für mich der einzige Fall, in dem man Code mit Eff (synchroner Effekt) oder Aff (asynchroner Effekt) schreibt und erst beim Aufruf entscheiden kann. Structured concurrency ist zwar cool, aber in der Praxis geht es dabei weniger um syntaktische Arbeit, um strukturierte Nebenläufigkeit zu erhalten, sondern eher darum, auf einem Server mehrere Top-Level-Request-Handler zu haben. Es ist letztlich nur ein Mittel, um Parallelverarbeitung einfacher zu machen

    • Ich weiß nicht, wie das auf der JVM implementiert wurde, aber allgemein ist Multithreading wirklich eine Technik, die sich intuitiv nur sehr schwer handhaben lässt. Es gibt unzählige Bücher über Race Conditions, Deadlocks, Livelocks, Starvation, Probleme der Speichersichtbarkeit und so weiter. Dagegen ist asynchrone Programmierung auf einem einzelnen Thread deutlich weniger belastend. Das Problem der Funktionsfärbung in Kauf zu nehmen ist immer noch weniger schmerzhaft, als „Heisenbugs“ in einer Multithread-Anwendung zu debuggen

    • Ich bin wirklich froh, dass Java diese Entscheidung getroffen hat

    • Die Erklärung dafür ist, dass normale Ausführung und asynchrone Funktionen jeweils geschlossene kartesische Kategorien bilden. Die Kategorie normaler Ausführung kann direkt in die asynchrone Kategorie eingebettet werden. Jede Funktion gehört zu einer Kategorie, also hat eine „Farbe“, und manche Sprachen zeigen das expliziter. Das ist eine Frage des Sprachdesigns, und Kategorientheorie lässt sich weit über Threading hinaus wirkungsvoll einsetzen. Java und threadbasierte Ansätze stehen dabei vor Synchronisationsproblemen, was besonders schwierig ist. JavaScript beschränkt sich innerhalb monadischer Kategorien insbesondere auf Continuation-passing-Stil

  • Als ich das Beispiel für using mit einer defer-Funktion gesehen habe, wirkte das sehr erfrischend. Für viele andere mag das schon intuitiv sein, aber ich finde es erwähnenswert

    • Wenn man DisposableStack und AsyncDisposableStack aus dem using-Vorschlag nutzt, bekommt man eingebaute Unterstützung für das Registrieren von Callbacks. Das ist nötig, weil using Block-Scope hat und man etwas für scope-übergreifende oder bedingte Registrierung braucht. Aber using-Variablen müssen ähnlich wie bei const sofort initialisiert werden, daher ist eine bedingte Initialisierung nicht möglich. In solchen Fällen braucht man das Muster, am Anfang der Funktion einen Stack anzulegen und genutzte Ressourcen per defer auf diesen Stack zu legen. Falls nötig, kann man den Freigabezeitpunkt damit leicht auf Funktionsebene verschieben

    • Fühlt sich ähnlich an wie in golang

  • Ich halte das für eine wirklich gute Idee, aber selbst wenn in Zukunft bei Web-API-Streams und Ähnlichem eine Vereinheitlichung von [Symbol.dispose] und [Symbol.asyncDispose] möglich sein sollte, werden in naher Zukunft nur einige APIs und Bibliotheken das unterstützen und der Rest, also die Mehrheit, nicht. Am Ende steht man vor dem Dilemma, using mit try/catch zu mischen oder einfach überall try/catch zu verwenden und dafür Code zu haben, der leichter verständlich ist. Dadurch besteht das Risiko, dass diese Funktion den Ruf bekommt, „praktisch unbenutzbar“ zu sein. Das ist schade, weil es eigentlich ein gutes Design ist, das ein reales Problem löst, aber trotzdem schwer einzuführen sein könnte

    • Für APIs, die so etwas nicht unterstützen, kann man DisposableStack verwenden und damit trotzdem using anwenden. Gerade wenn man mehrere Ressourcen zusammen behandelt, wird es viel einfacher als mit try/catch. Wenn nur die Runtime es unterstützt, kann man es sofort nutzen, ohne darauf warten zu müssen, dass bestehende Ressourcen aktualisiert werden

    • In der JavaScript-Welt wiederholt sich diese Situation seit 15 Jahren. Neue Sprachfeatures landen zuerst in Compilern wie Babel, dann in der Spezifikation und oft erst 3 bis 4 Jahre später in stabilen APIs und Browsern. Entwickler sind ohnehin daran gewöhnt, Web-APIs mit kleinen Wrappern zu umhüllen, und oft sind Wrapper besser als Polyfills. Ich habe bei nützlichen neuen Sprachfeatures noch nie gedacht: „Das wird sicher schwer zu benutzen sein“

    • Tatsächlich sind viele Funktionen bereits als Polyfill implementiert, und ein großer Teil des NodeJS-Ökosystems verwendet dieses Muster bereits; die Nutzer passen dann nur noch die Syntax mit einem Transpiler an. Als ich letztes Jahr einen Vortrag dazu vorbereitet habe, habe ich entdeckt, dass es in NodeJS und wichtigen Bibliotheken schon ziemlich viele APIs mit Symbol.dispose-Unterstützung gibt. Im Frontend wird es wegen vorhandener Lifecycle-Management-Systeme vermutlich weniger genutzt, aber in manchen Situationen bleibt es trotzdem nützlich. In Testbibliotheken und im Backend wird es sich meiner Meinung nach ausreichend verbreiten

    • TC39 sollte sich auch stärker auf grundlegende Sprachfeatures wie Traits/Protokolle à la Rust konzentrieren. In Rust kann man relativ leicht neue Traits definieren und implementieren, und in JS als dynamischer Sprache mit eindeutigen Symbolen ließe sich so etwas noch viel einfacher einführen. Es gibt Nachteile wie die Orphan Rule, aber daraus könnte sich eine deutlich flexiblere Struktur entwickeln

    • In der JavaScript-Welt löst man solche Situationen normalerweise mit Polyfills

  • Das erinnert an C#. Über IDisposable und IAsyncDisposable ist das für Abstraktionen wie Lock-Management, Queues oder temporäre Scopes sehr nützlich

    • Der Autor des Vorschlags kommt von Microsoft, daher wurde die Syntax ähnlich wie in C# festgelegt. Auch in den zugehörigen GitHub-Issues ist dieser Kontext konsistent zu sehen

    • Im Grunde ist das ein aus C# übernommenes Design. Der ursprüngliche Vorschlag verweist auch auf den Context Manager in Python, try-with-resources in Java und das using-Statement in C#. Das using-Schlüsselwort und die dispose-Hook-Methode sind ziemlich deutliche Hinweise

  • Ich verstehe, dass JavaScript auf Abwärtskompatibilität achten muss, aber die Syntax [Symbol.dispose]() wirkt auf mich etwas seltsam. Das verwirrt mich fast so, als läge dort ein Method-Handle in einem Array. Ich würde gern besser verstehen, was genau diese Syntax ist

    • Dazu die Erklärung, dass dynamisch berechnete Schlüssel auf der linken Seite in Objektliteralen, die in eckige Klammern gesetzt werden, seit ES6 schon fast zehn Jahre lang verwendet werden. Außerdem können Symbole nicht per String referenziert werden, daher kombiniert man dynamische Schlüssel mit der Kurzsyntax für Methoden. Im Kern ist das also keine neue Syntax

    • Mit guten Belegen dazu: Das stammt aus der Art, wie man Symbol-Schlüssel existierenden Objekten zuweist. Es ist ein natürlicher Ablauf

    • Andere Nutzer haben zwar schon erklärt, was es ist, aber offenbar nicht, warum es so ist. Wenn man für Methodennamen Symbol verwendet, ist garantiert, dass es sich um eine neue API handelt, ohne mit vorhandenen Methoden zu kollidieren. Das verhindert auch, dass eine Klasse versehentlich als disposable behandelt wird

    • Erwähnung des Konzepts des dynamischen Property-Zugriffs. Auf Objekteigenschaften kann man mit Punkt (.) oder eckigen Klammern ([]) zugreifen, sowohl mit Strings als auch mit Symbolen. Symbole sind eindeutige Objekte, die per Vergleich unterschieden werden, und durch well-known symbols wie Symbol.dispose bleibt Erweiterbarkeit gewährleistet. Außerdem wird erklärt, dass das konzeptionell Python-Methoden mit doppeltem Unterstrich ähnelt

    • Diese Syntax wird bereits seit mehreren Jahren verwendet. Auch die Iteratoren in JavaScript funktionieren so, und das wurde schon vor fast zehn Jahren eingeführt

  • Vorstellung der Motivation, warum man sich bemüht hat, strukturierte Nebenläufigkeit in JS einzuführen, besonders im Kontext von Ressourcenmanagement und lexikalischem Scope. Dabei wurde auch eine zugehörige Bibliothek für strukturierte Nebenläufigkeit geteilt

  • Ab Bun 1.0.23 wird das Feature bereits unterstützt. Man kann also experimentell damit spielen

  • Ich frage mich ernsthaft, wie man mit einem derart komplizierten Codestil den Programmablauf überhaupt verstehen und kontrollieren kann

    • Genau das ist der Punkt. 90 % der Webentwicklung bestehen aus nutzlosen oder unerwünschten Upgrades, und anschließend verbringt man 10 % der Zeit damit, die dadurch entstandenen Probleme wieder zu beheben. Mit geringer Wahrscheinlichkeit muss sich dann irgendwann doch jemand alten Code ansehen, und dafür kann man den Bug hervorragend als Einstiegsaufgabe für einen Junior hinterlassen. Selbst 20 Jahre alte Legacy-Systeme werden ja immer noch genutzt

    • Der als Beispiel gezeigte Code hat viele gravierende Syntaxfehler und ist weit von echtem JS entfernt. Und JS-Entwickler mischen solche Dinge in der Praxis nicht wild durcheinander wie while, Promise-Chains und finally; üblich ist eher await oder eine passende Exception-Handling-Struktur. In gut designten Bibliotheken stapelt man auch nicht mehrere Ebenen von Handlern übereinander, sondern kann mit DisposableStack wesentlich knapper schreiben. Heutzutage braucht man oft nicht einmal mehr eine sofort ausgeführte async-Funktion

    • Wenn man professionell mit einer Sprache arbeitet und sich an Bedeutung und Verhalten ihrer Schlüsselwörter gewöhnt, versteht man den Code ganz natürlich. Haskell-Programmierer gewöhnen sich auf ähnliche Weise ebenfalls daran

    • Beim Einbetten von Code auf HN muss man jede Zeile um mindestens zwei Leerzeichen einrücken. (Ich stimme aber zu, dass der Code schwer zu verstehen ist)

    • Der knappe Ratschlag, dass Einrückung hilft

  • Ich frage mich, warum man nicht einen anonymen Klassen-Destruktor gewählt hat oder etwas anderes als Symbole. Wenn es zwei Symbole gibt, eines für synchron und eines für asynchron, dann leckt die Abstraktion wieder

    • Ein Destruktor braucht vorhersagbares Verhalten, also klar definiertes Cleanup, und moderne Garbage Collector passen nicht zu diesem Muster. Moderne Sprachen unterstützen scope-basiertes Cleanup und setzen das über HoFs, spezielle Hooks, Callback-Registrierung und andere Wege um. Python war anfangs wegen referenzzählungsbasiertem GC an Destruktoren orientiert, hat aber wegen der Grenzen dieses Ansatzes Context Manager eingeführt

    • Destruktoren in anderen Sprachen laufen nach dem Timing des GC und sind daher unzuverlässig. Eine dispose-Methode dagegen wird eindeutig aufgerufen, wenn der Variablen-Scope endet, also vorhersehbar etwa zum Schließen von Dateien oder Freigeben von Locks. Symbolbasierte Methoden vermeiden Konflikte mit bestehenden Funktionen, und normalerweise müssen sich vor allem Bibliotheksentwickler darum kümmern. Die Unterscheidung zwischen synchron und asynchron muss eindeutig sein, und dafür kann eine etwas ungewohnte Syntax wie await using a = await b() nötig sein

    • In GC-Sprachen lassen sich Destruktoren kaum synchron aufrufen, daher ist ihr Verhalten meist nichtdeterministisch. In JS gibt es zwar WeakRef und FinalizationRegistry, aber selbst Mozilla empfiehlt ihre Verwendung wegen der Unvorhersehbarkeit nicht

    • Ein Vorteil dieses Ansatzes ist, dass er auch auf Ziele angewendet werden kann, die keine Klasseninstanzen sind

    • In JavaScript gibt es kein Konzept anonymer Eigenschaften, daher wirkt die Frage selbst etwas unklar. Es wird behauptet, dass es außer dieser Methode keine Alternative gibt

  • Das erste Beispiel im Vorschlag zeigt Code, der mit try/finally einen Lock sicher freigibt. Ich frage mich, ob solche Muster nur in Situationen mit langer Laufzeit wichtig sind oder ob in Browsern oder CLI-Umgebungen der Lock auch dann freigegeben wird, wenn der Prozess wegen eines Fehlers endet

    • Laut Spezifikation wird dispose immer ausgeführt, egal ob die Blockausführung normal endet oder durch Ausnahme, Verzweigung oder Verlassen des Blocks. In dieser Hinsicht sind using und try/finally also gleich. Ein erzwungenes Beenden des Prozesses liegt außerhalb des Zuständigkeitsbereichs der Spezifikation, daher greift ECMAScript dort nicht ein. Der Stream im Beispiel ist ein internes JS-Objekt; wenn der Interpreter verschwindet, verliert auch das Konzept eines Locks dort seine Bedeutung. Bei OS-Ressourcen wie Speicher oder Dateien räumt das Betriebssystem normalerweise gesammelt auf, aber das Verhalten ist plattformabhängig

    • Eine Browser-Webseite ist, anders betrachtet, eine Anwendung mit sehr langer Laufzeit. Sie läuft mitunter sogar länger als ein Serverprozess. Bei einem Fehler stirbt die Seite nicht einfach, und Fehlerbehandlung einschließlich Exceptions folgt klaren Regeln, bei denen finally greift. In NodeJS endet der Prozess bei Fehlern standardmäßig zwar oft, aber je nach Serverkontext sind andere Behandlungen üblich. Das heißt: Die Freigabefunktion in finally wird auf jeden Fall aufgerufen

 
ahwjdekf 2025-05-18

Bisher sind wir doch bestens klargekommen, ohne uns einen Deut um solchen Ressourcen-Kram zu scheren. Was ist denn plötzlich mit dir los?