- 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
await using data = await fn()Das Wunder, dass
awaitsowohl auf der linken als auch auf der rechten Seite stehthttps://typescriptlang.org/docs/handbook/…
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.disposeundSymbol.asyncDisposesowie anDisposableStackundAsyncDisposableStack. 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 verringertDem 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) oderAff(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 machenIch 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
usingmit einerdefer-Funktion gesehen habe, wirkte das sehr erfrischend. Für viele andere mag das schon intuitiv sein, aber ich finde es erwähnenswertWenn man
DisposableStackundAsyncDisposableStackaus demusing-Vorschlag nutzt, bekommt man eingebaute Unterstützung für das Registrieren von Callbacks. Das ist nötig, weilusingBlock-Scope hat und man etwas für scope-übergreifende oder bedingte Registrierung braucht. Aberusing-Variablen müssen ähnlich wie beiconstsofort 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 perdeferauf diesen Stack zu legen. Falls nötig, kann man den Freigabezeitpunkt damit leicht auf Funktionsebene verschiebenFü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,usingmit 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önnteFür APIs, die so etwas nicht unterstützen, kann man
DisposableStackverwenden und damit trotzdemusinganwenden. 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 werdenIn 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 verbreitenTC39 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
IDisposableundIAsyncDisposableist das für Abstraktionen wie Lock-Management, Queues oder temporäre Scopes sehr nützlichDer 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-resourcesin Java und dasusing-Statement in C#. Dasusing-Schlüsselwort und diedispose-Hook-Methode sind ziemlich deutliche HinweiseIch 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 istDazu 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
Symbolverwendet, 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 wirdErwä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 wieSymbol.disposebleibt Erweiterbarkeit gewährleistet. Außerdem wird erklärt, dass das konzeptionell Python-Methoden mit doppeltem Unterstrich ähneltDiese 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 undfinally; üblich ist eherawaitoder eine passende Exception-Handling-Struktur. In gut designten Bibliotheken stapelt man auch nicht mehrere Ebenen von Handlern übereinander, sondern kann mitDisposableStackwesentlich knapper schreiben. Heutzutage braucht man oft nicht einmal mehr eine sofort ausgeführte async-FunktionWenn 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 wieawait using a = await b()nötig seinIn GC-Sprachen lassen sich Destruktoren kaum synchron aufrufen, daher ist ihr Verhalten meist nichtdeterministisch. In JS gibt es zwar
WeakRefundFinalizationRegistry, aber selbst Mozilla empfiehlt ihre Verwendung wegen der Unvorhersehbarkeit nichtEin 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
disposeimmer ausgeführt, egal ob die Blockausführung normal endet oder durch Ausnahme, Verzweigung oder Verlassen des Blocks. In dieser Hinsicht sindusingund 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ängigEine 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
finallygreift. In NodeJS endet der Prozess bei Fehlern standardmäßig zwar oft, aber je nach Serverkontext sind andere Behandlungen üblich. Das heißt: Die Freigabefunktion infinallywird auf jeden Fall aufgerufenBisher sind wir doch bestens klargekommen, ohne uns einen Deut um solchen Ressourcen-Kram zu scheren. Was ist denn plötzlich mit dir los?