- A proposta de Explicit Resource Management introduz uma nova forma de controlar com clareza o ciclo de vida de recursos como identificadores de arquivo, conexões de rede e outros
- O recurso está disponível a partir do Chromium 134 e do V8 v13.8
- O que é adicionado à linguagem
- As declarações
using e await using, junto com os símbolos Symbol.dispose e Symbol.asyncDispose, fornecem um mecanismo de limpeza automática
DisposableStack e AsyncDisposableStack agrupam e liberam vários recursos com segurança
SuppressedError gerencia em conjunto os erros ocorridos durante a limpeza e o erro original
- Essa abordagem aumenta bastante a segurança e a manutenibilidade do código, além de ser eficaz para evitar vazamentos de recursos
- Ela simplifica o padrão tradicional
try...finally e permite um tratamento de recursos confiável em ambientes grandes e complexos com múltiplos recursos
Visão geral da proposta de gerenciamento explícito de recursos
- A proposta de Explicit Resource Management introduz uma nova forma de criar e liberar com clareza recursos como identificadores de arquivo e conexões de rede
- Os principais componentes são os seguintes
- Declarações
using e await using: liberam recursos automaticamente ao sair do escopo
- Símbolos
[Symbol.dispose]() e [Symbol.asyncDispose]() : métodos para implementar a ação de liberação (cleanup)
- Objetos globais DisposableStack e AsyncDisposableStack: agrupam vários recursos para gerenciamento eficiente
SuppressedError: novo tipo de erro que inclui tanto o erro ocorrido durante a limpeza quanto o erro original
- Esses recursos têm como foco permitir que desenvolvedores façam um gerenciamento detalhado de recursos e melhorem o desempenho e a segurança do código
Declarações using e await using
- A declaração
using é usada para recursos síncronos, e await using para recursos assíncronos
- Os recursos declarados chamam automaticamente Symbol.dispose ou Symbol.asyncDispose ao saírem do escopo
- Com isso, é possível reduzir problemas de vazamento em recursos síncronos e assíncronos e escrever código de liberação consistente
- Essas palavras-chave só podem ser usadas dentro de blocos de código, loops
for e corpos de função, e não podem ser usadas no nível superior
- Exemplo
- Por exemplo, ao usar
ReadableStreamDefaultReader, é necessário chamar reader.releaseLock() para que o stream possa ser reutilizado
- Se ocorrer um erro e essa chamada for omitida, pode surgir o problema de o stream ficar bloqueado permanentemente
- Forma tradicional
- Desenvolvedores usam um bloco try...finally para garantir a liberação do bloqueio do leitor
- É necessário escrever
reader.releaseLock() no bloco finally
- Forma aprimorada: introdução de
using
- Cria-se um objeto descartável (
readerResource) que inclui a ação de liberação
- Ao usar o padrão
using readerResource = {...}, a liberação acontece automaticamente assim que o bloco é deixado
- No futuro, se as Web APIs passarem a oferecer suporte a
[Symbol.dispose] e [Symbol.asyncDispose], poderá ser possível fazer esse gerenciamento automaticamente sem escrever objetos wrapper separados
DisposableStack e AsyncDisposableStack
DisposableStack e AsyncDisposableStack são introduzidos para agrupar vários recursos de forma eficiente e segura
- Ao adicionar recursos a cada stack e liberar o próprio stack, todos os recursos internos são liberados em ordem inversa
- Isso reduz riscos e simplifica o código ao lidar com conjuntos complexos de recursos que têm dependências entre si
- Principais métodos
use(value): adiciona um recurso descartável ao topo da stack
adopt(value, onDispose): adiciona um recurso não descartável junto com um callback de liberação
defer(onDispose): adiciona apenas a ação de liberação, sem um recurso
move(): move todos os recursos da stack atual para uma nova stack, permitindo transferir a propriedade
dispose(), asyncDispose(): liberam todos os recursos contidos na stack
Status de suporte e quando já pode ser usado
- O recurso de gerenciamento explícito de recursos está disponível no Chromium 134 e no V8 v13.8 ou superiores
- Há expectativa de expansão futura da compatibilidade com diversas Web APIs
4 comentários
await using data = await fn()o milagre de ter
awaittanto no lado esquerdo quanto no lado direitohttps://typescriptlang.org/docs/handbook/…
Comentários no Hacker News
Esta proposta passa uma sensação parecida com o problema das "cores das funções". A distinção entre funções síncronas e assíncronas continua invadindo todos os recursos. Por exemplo, dá para ver isso nos casos de
Symbol.disposeeSymbol.asyncDispose,DisposableStackeAsyncDisposableStack. Fico satisfeito que o Java tenha seguido para virtual threads. Acho que foi uma escolha de adicionar complexidade à JVM para reduzir a carga sobre desenvolvedores de aplicações, autores de bibliotecas e ferramentas de depuraçãoNão concordo com esconder a assincronicidade, porque isso tornaria mais difícil entender o fluxo do código. Eu também quero saber se um recurso é liberado de forma assíncrona e se isso pode ser afetado por fatores externos, como problemas de rede
Hoje em dia me irrita demais esse fenômeno de, na maioria das linguagens, "o normal é escrever todo o código como assíncrono". Vejo o Purescript como o único caso em que você escreve com Eff (efeitos síncronos) ou Aff (efeitos assíncronos) e pode escolher no ponto da chamada. Structured concurrency é legal, mas na prática parece menos um trabalho sintático para obter concorrência estruturada e mais um trabalho para ter vários handlers de requisições de topo em um servidor. Não passa de um meio para facilitar processamento paralelo
Não sei como isso foi implementado na JVM, mas, de modo geral, multithreading é uma tecnologia realmente pouco intuitiva de lidar. Há inúmeros livros sobre race conditions, deadlocks, livelocks, starvation, problemas de visibilidade de memória e assim por diante. Comparado a isso, programação assíncrona em thread única é bem menos pesada. Aceitar o problema das cores das funções parece uma escolha menos dolorosa do que depurar "Heisenbugs" em apps multithread
Fico realmente feliz que o Java tenha tomado essa decisão
A explicação é que execução normal e funções assíncronas formam closed Cartesian categories entre si. A categoria de execução normal pode ser embutida diretamente na categoria assíncrona. Toda função tem uma categoria, isto é, uma cor de função, e algumas linguagens deixam isso mais explícito. Isso é uma escolha de design da linguagem, e teoria das categorias pode ser aplicada com força muito além de threading. O Java e a abordagem baseada em threads acabam enfrentando problemas de sincronização, e isso tem a característica de ser especialmente difícil. O JavaScript restringe as categorias monádicas, em especial no estilo continuation-passing
Quando vi o exemplo de uso de
usingcom uma função defer, achei muito inovador. Talvez para muita gente isso já seja intuitivo, mas ainda assim acho que vale mencionarO uso de
DisposableStackeAsyncDisposableStack, incluídos na proposta deusing, dá suporte embutido para registrar callbacks. Comousingtem escopo de bloco, isso é necessário para atravessar escopos ou fazer registro condicional. Mas variáveisusing, comoconst, precisam ser inicializadas imediatamente, então não dá para fazer inicialização condicional. Nesses casos, é preciso o padrão de criar uma Stack no topo da função e colocar nela, com defer, os recursos usados. Se necessário, também é fácil ajustar o momento da liberação para o nível da funçãoLembra bastante golang
Acho uma ideia muito boa, mas, mesmo que no futuro seja possível unificar
[Symbol.dispose]e[Symbol.asyncDispose]em coisas como streams de Web API, no futuro próximo só parte das APIs e bibliotecas vai oferecer suporte ao recurso, enquanto o restante, provavelmente a maioria, não vai. No fim, surge o dilema entre misturarusingcom try/catch ou simplesmente usar try/catch em todo o código e escolher algo mais fácil de entender. Há o risco de isso render ao recurso a fama de "não dá para usar na prática". É uma pena, porque é um bom design para resolver um problema real, mas a adoção pode ser difícilEm APIs que não suportam esse recurso, dá para aplicar
usingcomDisposableStack. A vantagem é que, ao lidar com vários recursos ao mesmo tempo, isso fica muito mais simples do que try/catch. Se o runtime der suporte, já dá para usar imediatamente sem esperar atualização dos recursos existentesNo mundo JavaScript isso se repete há 15 anos. Recursos novos da linguagem primeiro entram em compiladores como Babel, depois na especificação, e por fim muitas vezes levam de 3 a 4 anos até aparecer em APIs estáveis e navegadores. Os desenvolvedores já estão acostumados a encapsular Web APIs com pequenos wrappers, e muitas vezes wrappers são melhores que polyfills. Nunca pensei "isso vai ser difícil de usar" só porque surgiu um novo recurso útil na linguagem
Na prática, muitos recursos já foram implementados como polyfill, então grande parte do ecossistema NodeJS usa esse padrão, e os usuários podem simplesmente ajustar a sintaxe com um transpiler. Quando preparei uma apresentação sobre isso no ano passado, descobri que NodeJS e bibliotecas importantes já tinham várias APIs com suporte a
Symbol.dispose. No frontend isso talvez seja menos usado por causa dos sistemas de gerenciamento de ciclo de vida, mas em algumas situações ainda é útil. Acho que vai se espalhar bastante em bibliotecas de teste e no backendO TC39 também precisa se concentrar em recursos fundamentais da linguagem, como traits/protocols ao estilo do Rust. No Rust é relativamente fácil definir e implementar um novo trait, enquanto no JS, por ser uma linguagem dinâmica e ter símbolos únicos, isso poderia ser introduzido de forma ainda mais simples. Há desvantagens, como a orphan rule, mas isso poderia evoluir para uma estrutura muito mais flexível
No mundo JavaScript normalmente isso se resolve com polyfills
Isso me lembra C#. Com
IDisposableeIAsyncDisposable, é muito útil para abstrações como gerenciamento de locks, filas e escopos temporáriosComo o autor da proposta veio da Microsoft, a sintaxe acabou ficando parecida com a de C#. Há um contexto consistente também nas issues relacionadas no GitHub
É basicamente um design emprestado do C#. A proposta original também faz referência ao context manager do Python, ao try-with-resources do Java e ao using statement do C#. A palavra-chave using e os métodos de hook de dispose já dão pistas bem fortes
Entendo que manter compatibilidade retroativa é importante no JavaScript, mas a sintaxe
[Symbol.dispose]()parece estranha. Dá a impressão de um array com handles de método. Fiquei curioso sobre o que exatamente é essa sintaxe e quero entender melhorA explicação é que chaves dinâmicas, envolvidas por colchetes no lado esquerdo de um objeto literal, já são usadas há quase 10 anos desde o ES6. Além disso, como símbolos não podem ser referenciados por string, combina-se chave dinâmica com sintaxe abreviada de método. No fundo, não é uma sintaxe nova
Com boas referências, a explicação é que isso vem da forma de atribuir chaves do tipo símbolo a objetos existentes. É uma evolução natural
Outras pessoas já explicaram o que é, mas talvez não tenham explicado o porquê. Usar um
Symbolcomo nome de método garante que isso seja uma nova API sem conflito com métodos existentes. Também evita que uma classe seja tratada por engano como disposableMenciona-se o conceito de dynamic property access. Propriedades de objeto podem ser acessadas com ponto (
.) ou colchetes ([]), e suportam tanto strings quanto símbolos. Símbolos são comparados como objetos únicos, e well known symbols, comoSymbol.dispose, garantem extensibilidade. Também foi explicada a semelhança com métodos__dunder__do PythonEssa sintaxe já é usada há vários anos. O iterator do JavaScript usa o mesmo esquema e isso foi introduzido há quase 10 anos
Apresentação do motivo de ter havido esforço para introduzir concorrência estruturada em JS, especialmente quando o gerenciamento de recursos com escopo léxico é uma característica importante. Também foi compartilhada uma biblioteca relacionada de concorrência estruturada
Já há suporte ao recurso no Bun a partir da versão 1.0.23. Dá para experimentar de forma prática
Não consigo entender de jeito nenhum como alguém pode compreender e controlar o fluxo de execução de um programa com um estilo de código tão complexo
Esse é exatamente o ponto. 90% do desenvolvimento web são upgrades inúteis ou que ninguém quer, e depois a realidade é gastar os outros 10% do tempo consertando os problemas que eles criaram. Existe uma pequena chance de alguém precisar olhar um código escrito anos atrás, e aí a ideia é deixar os bugs como tarefa de entrada para um iniciante. Até sistemas legados de 20 anos continuam em uso
O código apresentado como exemplo tem muitos erros graves de sintaxe e está bem distante de JavaScript real. E desenvolvedores JS normalmente não misturam as coisas desse jeito, como
while, promise chain efinally; o normal é usarawaitou uma estrutura apropriada de tratamento de exceções. Em bibliotecas bem projetadas, também não se empilha camadas e mais camadas de handlers, e é possível escrever de forma bem mais concisa comDisposableStack. Hoje em dia, muitas vezes nem funções async imediatamente invocadas são mais necessáriasSe você trabalha profissionalmente com a linguagem e se acostuma com o significado e o comportamento das palavras-chave, acaba entendendo o código naturalmente. Programadores Haskell se acostumam da mesma forma
Ao incorporar código no HN, é preciso indentar cada linha com pelo menos 2 espaços. (Concordo que o código é difícil de entender)
A dica concisa de que indentação ajuda
Fiquei curioso sobre por que não seguiram com destrutores de classe anônima, ou com alguma estrutura que não usasse
Symbol. Se existem doisSymbol(síncrono/assíncrono), isso levanta a questão de abstrações vazaremDestrutores exigem comportamento previsível, em que o cleanup seja claro, e coletores de lixo avançados não combinam com esse padrão. Linguagens modernas dão suporte a cleanup baseado em escopo e implementam isso de várias formas, como HoF, hooks especiais e registro de callbacks. O Python no início se baseava em destrutores com GC por contagem de referência, mas por limitações acabou introduzindo context managers
Em outras linguagens, destrutores rodam conforme o timing do GC, então não são confiáveis. Já métodos dispose são chamados de forma clara quando o escopo da variável termina, então são previsíveis para coisas como fechar arquivos ou liberar locks. Métodos baseados em
Symbolevitam conflitos com recursos existentes e, em geral, basta o autor da biblioteca se preocupar com isso. A distinção entre síncrono e assíncrono precisa ser explícita, e pode exigir uma sintaxe um pouco estranha, comoawait using a = await b()Em linguagens com GC, destrutores costumam ser majoritariamente não determinísticos porque é difícil fazer chamadas síncronas. No JS existem
WeakRefeFinalizationRegistry, mas nem a Mozilla recomenda o uso por serem imprevisíveisUm ponto forte dessa abordagem é que ela pode ser usada também em alvos que não são instâncias de classe
No JavaScript não existe o conceito de propriedade anônima, então a própria pergunta parece ambígua. A alegação é que não haveria alternativa além desse método
O primeiro exemplo da proposta mostra um caso de código que libera um lock com segurança usando try/finally. Fiquei em dúvida se esse padrão só importa em situações de longa execução, ou se, em navegador ou ambiente CLI, o lock também seria liberado quando o processo terminasse por erro
A especificação diz que, quer a execução do bloco termine normalmente, quer termine por exceção, desvio ou saída, o dispose sempre será executado. Ou seja, é o mesmo tanto com using quanto com try/finally. Encerramento forçado, como matar o processo à força, fica fora do escopo da especificação, então o ECMAScript não trata disso. O stream do exemplo é um objeto interno do JS, então, se o interpretador desaparecer, a própria ideia de lock deixa de fazer sentido. Se forem recursos do sistema operacional, como memória e arquivos, normalmente o SO faz uma limpeza geral, mas o comportamento varia conforme a plataforma
Uma página web no navegador, por outro lado, é de certa forma uma aplicação de longa execução. Em alguns casos ela roda até mais do que um processo de servidor. Quando ocorre um erro, a página não morre por isso, e o tratamento de erro, inclusive de exceções, segue regras claras e passa pelo
finally. No NodeJS, por padrão, o processo encerra em caso de erro, mas em ambientes de servidor é comum haver tratamento diferente. Ou seja, a função de liberação nofinallycertamente será chamadaAté hoje a gente viveu muito bem sem dar a mínima para esse negócio de gerenciar recursos. Por que essa mudança repentina?