7 điểm bởi GN⁺ 2025-05-18 | 4 bình luận | Chia sẻ qua WhatsApp
  • Đề xuất Explicit Resource Management mang đến một cách mới để kiểm soát rõ ràng vòng đời của các tài nguyên như file handle, kết nối mạng, v.v.
  • Có thể sử dụng tính năng này từ Chromium 134 và V8 v13.8
  • Các phần được bổ sung vào ngôn ngữ
    • Cung cấp cơ chế dọn dẹp tự động thông qua khai báo usingawait using, cùng với các symbol Symbol.dispose, Symbol.asyncDispose
    • DisposableStack, AsyncDisposableStack giúp nhóm và giải phóng nhiều tài nguyên một cách an toàn
    • SuppressedError quản lý đồng thời lỗi phát sinh trong lúc dọn dẹp và lỗi gốc
  • Cách tiếp cận này giúp tăng đáng kể độ an toàn và khả năng bảo trì của mã, đồng thời hiệu quả trong việc ngăn rò rỉ tài nguyên
  • Đơn giản hóa mẫu try...finally truyền thống và cho phép xử lý tài nguyên đáng tin cậy trong các môi trường tài nguyên phức hợp quy mô lớn

Tổng quan về đề xuất quản lý tài nguyên tường minh

  • Đề xuất Explicit Resource Management giới thiệu một cách mới để tạo và giải phóng rõ ràng các tài nguyên như file handle, kết nối mạng, v.v.
  • Các thành phần chính gồm có
    • Khai báo usingawait using: tự động giải phóng tài nguyên khi thoát khỏi scope
    • Các symbol [Symbol.dispose](), [Symbol.asyncDispose]() : phương thức để triển khai hành vi giải phóng (cleanup)
    • Các đối tượng toàn cục DisposableStack, AsyncDisposableStack: nhóm nhiều tài nguyên để quản lý hiệu quả
    • SuppressedError: kiểu lỗi mới chứa cả lỗi phát sinh trong quá trình dọn dẹp tài nguyên và lỗi ban đầu
  • Các tính năng này tập trung vào việc giúp lập trình viên quản lý tài nguyên ở mức chi tiết, đồng thời cải thiện hiệu năng và độ an toàn của mã

Khai báo usingawait using

  • Khai báo using được dùng cho tài nguyên đồng bộ, còn await using dùng cho tài nguyên bất đồng bộ
  • Tài nguyên đã khai báo sẽ tự động gọi [Symbol.dispose()] hoặc [Symbol.asyncDispose()] khi ra khỏi scope
  • Nhờ đó có thể giảm vấn đề rò rỉ tài nguyên đồng bộ/bất đồng bộ và viết mã giải phóng nhất quán
  • Các từ khóa này chỉ có thể dùng trong code block, vòng lặp for, hoặc thân hàm; không thể dùng ở top-level
  • Ví dụ
    • Chẳng hạn khi dùng ReadableStreamDefaultReader, bắt buộc phải gọi reader.releaseLock() thì stream mới có thể được tái sử dụng
    • Nếu xảy ra lỗi mà lời gọi này bị bỏ sót, stream có thể bị khóa vĩnh viễn
  • Cách truyền thống
    • Lập trình viên dùng khối try...finally để đảm bảo mở khóa reader
    • Cần viết mã reader.releaseLock() trong khối finally
  • Cách cải tiến: đưa vào using
    • Tạo một đối tượng disposable (readerResource) có chứa hành vi giải phóng
    • Dùng mẫu using readerResource = {...} thì tài nguyên sẽ tự động được giải phóng ngay khi thoát khỏi code block
    • Trong tương lai, nếu Web API hỗ trợ [Symbol.dispose][Symbol.asyncDispose], có thể quản lý tự động mà không cần viết đối tượng wrapper riêng

DisposableStack và AsyncDisposableStack

  • DisposableStackAsyncDisposableStack được đưa vào để nhóm nhiều tài nguyên một cách hiệu quả và an toàn
  • Có thể thêm tài nguyên vào từng stack, và khi giải phóng chính stack đó thì toàn bộ tài nguyên bên trong sẽ được giải phóng theo thứ tự ngược lại
  • Cách này giúp giảm rủi ro và đơn giản hóa mã khi xử lý các tập tài nguyên phức tạp có quan hệ phụ thuộc
  • Các phương thức chính
    • use(value): thêm một tài nguyên disposable lên đỉnh stack
    • adopt(value, onDispose): gắn callback giải phóng cho tài nguyên không phải disposable rồi thêm vào
    • defer(onDispose): chỉ thêm hành vi giải phóng mà không cần tài nguyên
    • move(): chuyển toàn bộ tài nguyên của stack hiện tại sang một stack mới để chuyển quyền sở hữu
    • dispose(), asyncDispose(): giải phóng toàn bộ tài nguyên trong stack

Tình trạng hỗ trợ và thời điểm có thể sử dụng

  • Có thể dùng tính năng quản lý tài nguyên tường minh trên Chromium 134, V8 v13.8 trở lên
  • Trong tương lai, tính năng này được kỳ vọng sẽ mở rộng khả năng tương thích với nhiều Web API khác nhau

4 bình luận

 
cichol 2025-05-18

await using data = await fn()
Điều kỳ diệu khi await xuất hiện ở cả vế trái lẫn vế phải

 
tested 2025-05-18

Siêu năng lực mới của JavaScript: quản lý tài nguyên tường minh

https://typescriptlang.org/docs/handbook/…

 
GN⁺ 2025-05-18
Ý kiến Hacker News
  • Đề xuất này tạo cảm giác giống với vấn đề "màu sắc của hàm". Sự phân biệt giữa hàm đồng bộ và bất đồng bộ tiếp tục len vào mọi tính năng. Ví dụ có thể thấy ở Symbol.dispose và Symbol.asyncDispose, DisposableStack và AsyncDisposableStack. Tôi hài lòng với việc Java chuyển sang virtual threads. Tôi nghĩ đó là lựa chọn giúp giảm gánh nặng cho lập trình viên ứng dụng, người viết thư viện và trình gỡ lỗi bằng cách thêm độ phức tạp vào JVM

    • Tôi không đồng ý ở điểm nếu che giấu tính bất đồng bộ thì sẽ càng khó hiểu luồng mã hơn. Tôi muốn biết liệu tài nguyên có được giải phóng bất đồng bộ hay không, và liệu nó có thể bị ảnh hưởng bởi yếu tố bên ngoài như sự cố mạng hay không

    • Thật sự rất khó chịu với hiện tượng "viết mọi thứ dưới dạng bất đồng bộ là lẽ thường" ở hầu hết các ngôn ngữ ngày nay. Tôi xem Purescript là trường hợp hiếm hoi cho phép viết bằng Eff (hiệu ứng đồng bộ) hoặc Aff (hiệu ứng bất đồng bộ) rồi chọn ở thời điểm gọi. Structured concurrency rất hay, nhưng trên thực tế nó gần như không phải là công việc cú pháp để đạt được structured concurrency, mà gần hơn với việc có nhiều top-level request handler trên server. Chỉ là một phương tiện để việc xử lý song song trở nên dễ hơn

    • Tôi không biết JVM triển khai điều đó thế nào, nhưng nói chung multithreading thật sự là công nghệ cực kỳ khó xử lý một cách trực quan. Có vô số sách viết về race condition, deadlock, livelock, starvation, vấn đề hiển thị bộ nhớ và nhiều thứ khác. So với điều đó, lập trình bất đồng bộ đơn luồng đỡ nặng đầu hơn nhiều. Chấp nhận vấn đề màu sắc của hàm vẫn ít đau đớn hơn việc gỡ lỗi "Heisenbug" trong ứng dụng đa luồng

    • Tôi thực sự vui vì Java đã đưa ra lựa chọn đó

    • Có lời giải thích rằng đó là vì thực thi thông thường và hàm bất đồng bộ tạo thành closed Cartesian categories khép kín với nhau. Category của thực thi thông thường có thể được nhúng trực tiếp vào category bất đồng bộ. Mọi hàm đều có category của nó, tức là màu sắc của hàm, và một số ngôn ngữ phơi bày điều này rõ ràng hơn. Đây là lựa chọn thiết kế ngôn ngữ, và lý thuyết category có thể được áp dụng rất mạnh mẽ vượt ra ngoài vấn đề luồng. Java và cách tiếp cận dựa trên thread sẽ phải đối mặt với các vấn đề đồng bộ hóa, và đó là phần đặc biệt khó. JavaScript giới hạn một category mang tính monad, cụ thể là kiểu Continuation-passing

  • Khi xem ví dụ dùng using với hàm defer, tôi thấy rất mới mẻ. Có thể với nhiều người khác thì nó đã trực quan rồi, nhưng tôi nghĩ vẫn đáng để nhắc tới

    • Nếu tận dụng DisposableStack và AsyncDisposableStack nằm trong đề xuất using, việc đăng ký callback được hỗ trợ sẵn. Vì using có block scope nên điều này cần thiết cho việc vượt qua scope hoặc đăng ký có điều kiện. Tuy nhiên biến using phải được khởi tạo ngay giống const, nên không thể khởi tạo có điều kiện. Trong trường hợp đó cần mẫu tạo Stack ở đầu hàm rồi đưa tài nguyên cần dùng vào stack bằng defer. Khi cần cũng có thể dễ dàng dời thời điểm giải phóng lên cấp độ toàn hàm

    • Cảm giác khá giống golang

  • Tôi nghĩ đây là ý tưởng rất hay, nhưng<p>ngay cả khi trong tương lai có thể hợp nhất [Symbol.dispose] và [Symbol.asyncDispose] ở các web API stream chẳng hạn, thì trong tương lai gần sẽ chỉ có một phần API và thư viện hỗ trợ, còn phần còn lại, tức đa số, thì không. Cuối cùng sẽ rơi vào thế lưỡng nan: hoặc trộn "using" với try/catch, hoặc dùng try/catch cho toàn bộ mã để chọn kiểu mã dễ hiểu hơn. Vì thế có nguy cơ tính năng này sẽ mang tiếng là "không dùng thực tế được". Thật đáng tiếc ở chỗ đây là thiết kế tốt để giải quyết vấn đề thật, nhưng có thể khó được chấp nhận

    • Với các API không hỗ trợ tính năng này, có thể dùng DisposableStack để áp dụng using. Khi xử lý nhiều tài nguyên cùng lúc, nó vẫn đơn giản hơn try/catch rất nhiều. Chỉ cần runtime hỗ trợ là có thể dùng ngay, không cần chờ các tài nguyên hiện có được cập nhật

    • Trong thế giới JavaScript, chuyện này lặp lại suốt 15 năm qua. Tính năng ngôn ngữ mới thường được đưa vào các compiler như Babel trước, sau đó mới vào spec, rồi phải mất thêm 3-4 năm nữa mới có API ổn định và được trình duyệt hỗ trợ. Dù sao thì các lập trình viên cũng đã quen với việc bọc web API bằng wrapper nhỏ, và nhiều khi wrapper còn tốt hơn polyfill. Tôi chưa từng nghĩ rằng có tính năng ngôn ngữ mới hữu ích nào lại "khó dùng" đến mức không thể áp dụng

    • Trên thực tế nhiều tính năng đã được triển khai bằng polyfill, nên phần lớn hệ sinh thái NodeJS đã dùng mẫu này, và người dùng chỉ cần chỉnh cú pháp bằng transpiler. Năm ngoái khi chuẩn bị một bài trình bày liên quan, tôi phát hiện NodeJS và nhiều thư viện lớn đã có khá nhiều API hỗ trợ Symbol.dispose. Ở frontend có lẽ nó ít được dùng hơn vì đã có hệ thống quản lý vòng đời, nhưng trong một số tình huống nó vẫn hữu ích. Tôi nghĩ trong thư viện test hoặc backend thì nó sẽ phổ biến đủ nhiều

    • TC39 cũng cần tập trung vào những tính năng ngôn ngữ nền tảng như trait/protocol của Rust. Trong Rust, việc định nghĩa và triển khai trait mới tương đối dễ, còn với JS là ngôn ngữ động có unique symbol thì việc đưa vào thậm chí còn đơn giản hơn nhiều. Có những nhược điểm như orphan rule, nhưng nó có thể phát triển thành cấu trúc linh hoạt hơn nhiều

    • Trong thế giới JavaScript thì thường giải quyết kiểu này bằng polyfill

  • Làm tôi nhớ tới C#. Thông qua IDisposable và IAsyncDisposable, nó cực kỳ hữu ích cho các abstraction như quản lý lock, queue, quản lý scope tạm thời, v.v.

    • Tác giả của đề xuất xuất thân từ Microsoft nên cú pháp được định hình tương tự C#. Trong các issue GitHub liên quan cũng có cùng một mạch ngữ cảnh như vậy

    • Về cơ bản là thiết kế vay mượn từ C#. Đề xuất ban đầu thực ra cũng tham chiếu đến context manager của Python, try-with-resources của Java, using statement của C# và nhiều thứ khác. Từ khóa using và phương thức hook dispose là gợi ý khá rõ

  • Tôi hiểu JavaScript cần giữ tương thích ngược, nhưng cú pháp [Symbol.dispose]() vẫn thấy hơi kỳ. Dễ gây nhầm như thể có method handle nằm trong mảng. Tôi tò mò muốn tìm hiểu thêm xem cú pháp này thực chất là gì

    • Có giải thích rằng dynamic key được bọc trong dấu ngoặc vuông ở vế trái của object literal đã được dùng gần 10 năm kể từ ES6. Ngoài ra symbol không thể được tham chiếu bằng chuỗi, nên phải kết hợp dynamic key với cú pháp rút gọn method. Về bản chất thì đây không phải cú pháp mới

    • Kèm theo tài liệu vững chắc, đây là cách bắt nguồn từ việc gán symbol key cho đối tượng hiện có. Một tiến trình phát triển tự nhiên

    • Người khác đã giải thích đó là gì rồi, nhưng dường như chưa ai giải thích tại sao lại như vậy. Nếu dùng Symbol làm tên method thì sẽ đảm bảo đó là API mới mà không xung đột với method có sẵn. Nó cũng giúp tránh việc một class vô tình bị coi là disposable

    • Có nhắc tới khái niệm dynamic property access. Thuộc tính đối tượng có thể được truy cập bằng dấu chấm (.) hoặc dấu ngoặc vuông ([]), và hỗ trợ cả chuỗi lẫn symbol. Symbol là đối tượng duy nhất được so sánh theo danh tính, và các well known symbol như "[Symbol.dispose]" giúp bảo đảm khả năng mở rộng. Khái niệm này cũng tương tự các phương thức dunder của Python

    • Cú pháp này đã được dùng nhiều năm rồi. Iterator của JavaScript cũng dùng cách tương tự và đã được đưa vào từ gần 10 năm trước

  • Giới thiệu lý do đã nỗ lực đưa structured concurrency vào JS để quản lý tài nguyên, đặc biệt khi lexical scope là đặc trưng. Đồng thời chia sẻ cả thư viện structured concurrency liên quan

  • Tính năng này đã được hỗ trợ trong Bun từ phiên bản 1.0.23 trở lên. Có thể thử nghiệm ngay

  • Tôi thật sự không hiểu làm sao có thể hiểu và kiểm soát luồng thực thi của chương trình với kiểu mã phức tạp thế này

    • Đó chính là vấn đề cốt lõi. 90% phát triển web là các bản nâng cấp vô dụng hoặc chẳng ai muốn, rồi lại dành 10% thời gian để sửa những vấn đề do chúng tạo ra. Thi thoảng sẽ có xác suất thấp ai đó phải đọc lại đoạn mã cũ, và đây là lúc có thể để bug lại như bài tập nhập môn cho người mới. Thậm chí các hệ thống legacy 20 năm tuổi vẫn còn đang được dùng

    • Đoạn mã được đưa ra làm ví dụ có rất nhiều lỗi cú pháp nghiêm trọng nên khá xa JavaScript thực tế. Và các lập trình viên JS cũng không trộn kiểu dùng như vậy giữa while, promise chain, finally... mà thường sẽ dùng await hoặc cấu trúc xử lý ngoại lệ phù hợp. Trong thư viện được thiết kế tốt, người ta cũng không xếp chồng nhiều lớp handler như vậy mà có thể viết gọn hơn bằng DisposableStack. Ngày nay thậm chí nhiều trường hợp không còn cần async IIFE nữa

    • Khi làm việc chuyên nghiệp với ngôn ngữ đó và đã quen với ý nghĩa cũng như hành vi của các từ khóa, bạn sẽ tự nhiên hiểu được mã. Lập trình viên Haskell cũng quen theo cách tương tự

    • Khi nhúng code trên HN thì cần thụt đầu dòng ít nhất 2 khoảng trắng cho mỗi dòng. (Tôi đồng ý là code khó hiểu)

    • Lời khuyên ngắn gọn rằng thụt đầu dòng sẽ giúp ích

  • Tò mò vì sao không chọn destructor của anonymous class, hoặc không dùng cấu trúc nào khác ngoài Symbol. Nếu tồn tại hai Symbol, một cho đồng bộ và một cho bất đồng bộ, thì có vẻ abstraction đang bị lộ ra

    • Destructor cần hành vi có thể dự đoán được, tức cleanup phải rõ ràng, nhưng GC tiên tiến lại không phù hợp với kiểu đó. Các ngôn ngữ hiện đại hỗ trợ cleanup theo scope và có thể triển khai bằng HoF, hook đặc biệt, đăng ký callback cùng nhiều cách khác. Python ban đầu dựa vào destructor theo refcount GC, nhưng do giới hạn nên đã đưa vào context manager

    • Destructor ở các ngôn ngữ khác chạy theo thời điểm GC nên không đáng tin cậy. Trong khi đó, phương thức dispose được gọi rõ ràng khi biến ra khỏi scope, nên có thể dự đoán được cho việc đóng file hay giải phóng lock. Phương thức dựa trên Symbol cũng tránh xung đột với các tính năng hiện có, và thường chỉ thư viện cần quan tâm. Việc phân biệt đồng bộ và bất đồng bộ phải rõ ràng, và có thể cần cú pháp hơi lạ như await using a = await b()

    • Trong ngôn ngữ có GC, destructor khó có thể được gọi đồng bộ nên đa số mang tính không xác định. JS có WeakRef và FinalizationRegistry, nhưng ngay cả Mozilla cũng không khuyến khích dùng vì không thể dự đoán

    • Điểm mạnh của cách này là có thể dùng cho cả những đối tượng không phải instance của class

    • JavaScript không có khái niệm anonymous property, nên bản thân câu hỏi đã hơi mơ hồ. Có ý kiến cho rằng ngoài cách này thì không có lựa chọn thay thế nào khác

  • Ví dụ đầu tiên trong bản đề xuất là mã dùng try/finally để giải phóng lock an toàn. Tôi thắc mắc liệu mẫu này chỉ quan trọng trong các tình huống chạy lâu hay không, và trong môi trường trình duyệt hoặc CLI, nếu tiến trình kết thúc vì lỗi thì lock có còn được giải phóng không

    • Theo đặc tả, dù block kết thúc bình thường hay kết thúc do ngoại lệ, nhánh rẽ hoặc thoát giữa chừng thì dispose vẫn luôn được gọi. Tức là using hay try/finally đều giống nhau. Còn việc bị cưỡng chế kết thúc như kill process nằm ngoài phạm vi đặc tả nên ECMAScript không can thiệp. Stream trong ví dụ là đối tượng nội bộ của JS, nên nếu interpreter biến mất thì bản thân khái niệm lock cũng mất ý nghĩa. Nếu là tài nguyên cấp OS như bộ nhớ, file v.v. thì thường OS sẽ dọn dẹp hàng loạt, nhưng hành vi cụ thể khác nhau theo từng nền tảng

    • Một trang web trên trình duyệt, xét theo cách khác, là ứng dụng chạy rất lâu. Thậm chí còn chạy lâu hơn nhiều tiến trình server. Khi lỗi xảy ra thì trang không chết ngay, và việc xử lý lỗi bao gồm ngoại lệ được thực hiện theo quy tắc rõ ràng trong finally. Trên NodeJS thì mặc định tiến trình sẽ thoát khi có lỗi, nhưng trong bối cảnh server thì cách xử lý khác cũng rất phổ biến. Nói cách khác, hàm giải phóng trong finally chắc chắn sẽ được gọi

 
ahwjdekf 2025-05-18

Trước giờ chúng ta vẫn sống ổn mà chẳng thèm bận tâm 1 chút nào đến mấy thứ tài nguyên kiểu này. Sao tự nhiên giờ cậu lại thế?