1 điểm bởi GN⁺ 2026-05-06 | 2 bình luận | Chia sẻ qua WhatsApp
  • Async Rust cho phép chạy cùng một đoạn mã độc lập với executor trên cả máy chủ và vi điều khiển, nhưng do state machine mà compiler tạo ra, kích thước binary tăng lên rất rõ, đặc biệt là trong môi trường embedded
  • Ngay cả ví dụ đơn giản như bar() với 2 điểm await cũng tạo ra 360 dòng MIR cùng các trạng thái Unresumed, Returned, Panicked, Suspend0, Suspend1, trong khi phiên bản đồng bộ chỉ cần 23 dòng
  • Nếu thay đổi để khi poll lại một future đã hoàn tất thì trả về Poll::Pending thay vì panic, vẫn có thể đáp ứng contract mà không gây hành vi unsafe, và trong thử nghiệm đã giúp giảm kích thước binary 2%~5% cho firmware embedded
  • Ngay cả async { 5 } không có await hiện tại cũng tạo state machine với mặc định 3 trạng thái, nhưng nếu tối ưu để luôn trả về Poll::Ready(5) thì kích thước binary embedded giảm 0,2%
  • Project Goal được đề xuất nhằm thúc đẩy trong compiler việc loại bỏ panic sau khi hoàn tất ở chế độ release, loại bỏ state machine cho async block không có await, inline future chỉ có một await, và gộp các trạng thái giống nhau

Vấn đề phình to ở cấp compiler của Async Rust

  • Async Rust cho phép chạy cùng một đoạn mã độc lập với executor trên cả máy chủ và vi điều khiển, nhưng trên các vi điều khiển nhỏ, mức tăng kích thước binary đặc biệt dễ thấy
  • Blog Rust từng giới thiệu async/await là abstraction không chi phí, nhưng trên thực tế async tạo ra rất nhiều bloat; vấn đề tương tự cũng tồn tại trên desktop và server, chỉ là ít lộ rõ hơn vì có nhiều bộ nhớ và tài nguyên tính toán hơn
  • Tiếp nối cách lách để tránh bloat khi viết mã async, một Project Goal đã được đệ trình để giải quyết vấn đề này ngay trong compiler
  • Vấn đề future lớn hơn mức cần thiết và bị sao chép quá nhiều không nằm trong phạm vi của bài này

Cấu trúc của future được tạo ra

  • Mã ví dụ có foo() trả về async { 5 }, còn bar() thực hiện foo().await + foo().await
  • bar có 2 điểm await nên state machine tối thiểu cần 2 trạng thái, nhưng trên thực tế còn tạo ra nhiều trạng thái hơn
  • Rust compiler có thể dump MIR ở nhiều pass khác nhau, và pass coroutine_resume là pass MIR cuối cùng dành riêng cho async
    • async vẫn còn trong MIR nhưng không còn trong LLVM IR, nên quá trình biến async thành state machine diễn ra ở các pass MIR
  • Hàm bar tạo ra 360 dòng MIR, trong khi phiên bản đồng bộ chỉ dùng 23 dòng
  • CoroutineLayout mà compiler in ra về bản chất là một tập trạng thái dưới dạng enum
    • Unresumed: trạng thái ban đầu
    • Returned: trạng thái đã hoàn tất
    • Panicked: trạng thái sau panic
    • Suspend0: điểm await đầu tiên, nơi lưu future của foo
    • Suspend1: điểm await thứ hai, nơi lưu kết quả đầu tiên và future foo thứ hai
    Quảng cáo
  • Future::poll là hàm an toàn, nên dù bị gọi lại sau khi future đã hoàn tất cũng không được gây ra UB
    • Hiện tại sau Suspend1, nó trả về Ready rồi chuyển future sang trạng thái Returned
    • Nếu poll lại ở trạng thái này thì sẽ xảy ra panic
  • Trạng thái Panicked dường như dùng để ngăn việc poll lại future sau khi hàm async panic rồi panic đó bị bắt bởi catch_unwind
    • Sau panic, future có thể ở trạng thái không hoàn chỉnh, nên poll lại có thể dẫn đến UB
    • Cơ chế này rất giống với mutex poisoning
    • Cách diễn giải này về trạng thái Panicked chỉ ở mức khoảng 90% chắc chắn vì khó tìm được tài liệu xác nhận rõ ràng

Có nhất thiết phải panic khi poll sau khi đã hoàn tất không?

  • Future ở trạng thái Returned hiện tại sẽ panic, nhưng không nhất thiết phải như vậy
    • Điều kiện cần chỉ là không gây ra UB
  • panic tương đối tốn kém, đồng thời thêm vào một nhánh có tác dụng phụ mà rất khó loại bỏ bằng tối ưu hóa
  • Nếu khi poll lại một future đã hoàn tất mà trả về Poll::Pending, vẫn có thể đáp ứng contract của kiểu Future mà không tạo ra hành vi unsafe
  • Khi thử nghiệm bằng cách chỉnh compiler theo hướng này, firmware embedded dùng async đã cho thấy giảm 2%~5% kích thước binary
  • Đề xuất là cung cấp hành vi này dưới dạng một tùy chọn, tương tự overflow-checks = false cho tràn số nguyên
    • Ở bản debug vẫn tiếp tục panic để lộ lỗi hành vi sai ngay lập tức
    • Ở bản release có thể nhận được future nhỏ hơn
  • Khi dùng panic=abort, có khả năng loại bỏ hoàn toàn trạng thái Panicked, nhưng tác động của việc này cần được xem xét thêm
Quảng cáo

Không có await nhưng vẫn luôn tạo state machine

  • foo() chỉ trả về async { 5 }, nên dạng tối ưu nhất nếu tự cài đặt bằng tay là một future không có trạng thái, luôn trả về Poll::Ready(5)
  • Nhưng MIR do compiler tạo ra vẫn có 3 trạng thái mặc định là Unresumed, Returned, Panicked
    • Khi poll, nó kiểm tra discriminant của trạng thái hiện tại rồi phân nhánh
    • Nếu poll lại sau khi đã hoàn tất thì sẽ panic với assert `async fn` resumed after completion
  • Trong trường hợp này có thể tối ưu để không tạo state machine mà chỉ luôn trả về Poll::Ready(5)
  • Khi áp dụng thử nghiệm vào compiler, kích thước binary embedded giảm 0,2%
    • Mức tiết kiệm không lớn, nhưng đây là tối ưu đơn giản nên vẫn có thể đáng để áp dụng
  • Tối ưu này có thay đổi hành vi một chút, nhưng chỉ ảnh hưởng đến các executor không tuân thủ quy ước
    • Compiler hiện tại sẽ panic ở lần poll sau
    • Sau tối ưu, future sẽ luôn trả về Ready

Chỉ LLVM là không đủ

  • Dù đầu ra MIR có kém hiệu quả, đôi khi LLVM vẫn có thể dọn dẹp hết, nhưng điều kiện khá hạn chế
    • future phải đủ đơn giản
    • phải dùng opt-level=3
  • Khi future phức tạp hơn, LLVM không thể loại bỏ hết, và vì mã async Rust theo kiểu thông dụng thường lồng future rất sâu nên độ phức tạp tăng rất nhanh
  • Trong các môi trường thường tối ưu theo kích thước như embedded hay wasm, LLVM không thể tối ưu hết mọi thứ
  • Ví dụ Godbolt: https://godbolt.org/z/58ahb3nne
    • Trong assembly được tạo ra, LLVM biết rằng foo trả về 5, nhưng không thể tối ưu đáp án của bar thành 10
    • Lời gọi hàm poll của foo vẫn còn đó
    • Lý do là tồn tại các nhánh panic tiềm năng mà compiler không thể xác quyết hoàn toàn
    • LLVM không biết rằng foo trên thực tế chỉ được gọi một lần và không panic
  • Nếu comment out nhánh panic trong IR thì tối ưu tốt hơn: https://godbolt.org/z/38KqjsY8E
  • Thay vì kỳ vọng LLVM tối ưu hậu kỳ, compiler nên cung cấp đầu vào tốt hơn cho LLVM
Quảng cáo

Inline future chưa hoạt động tốt

  • Inline rất quan trọng vì nó mở đường cho các pass tối ưu hóa tiếp theo, nhưng các future Rust được tạo ra hiện chưa được inline ở giai đoạn sớm
  • Chỉ sau khi mỗi future có phần hiện thực riêng thì LLVM và linker mới có cơ hội inline, nhưng do những vấn đề ở trên nên lúc đó đã quá muộn
  • Cơ hội inline trực tiếp nhất là khi bar() chỉ đơn thuần thực hiện foo(blah).await
    • Đây là mẫu rất hay gặp khi dùng trait để tạo lớp trừu tượng
    • Compiler hiện tại tạo state machine cho bar, rồi từ trong đó gọi state machine của foo
    • Hiệu quả hơn là để bar chính là future của foo
  • Trường hợp có preamble và postamble thì phức tạp hơn
    • Ví dụ: bar(input) tạo blah từ input > 10, sau đó foo(blah).await rồi áp dụng * 2 lên kết quả
    • Mẫu này rất phổ biến khi chuyển đổi async function sang chữ ký khác, đặc biệt trong triển khai trait
  • Dạng bar này cũng không nhất thiết cần trạng thái async riêng
    • Không có dữ liệu nào cần được giữ qua điểm await duy nhất ngoài giá trị đã được foo capture
    • Tuy vậy bar không thể đơn giản trở thành chính foo, mà phần lớn trạng thái có thể dựa vào foo
  • Nếu tự cài đặt bằng tay, BarFut có thể có các trạng thái Unresumed { input }Inlined { foo: FooFut }
    • Ở lần poll đầu tiên, nó chạy preamble để tạo foo(blah) rồi chuyển sang trạng thái Inlined
    • Sau đó áp dụng postamble lên kết quả của foo.poll(cx)
  • Nếu có thể thực thi sẵn mã trước điểm await đầu tiên thì còn có thể bỏ luôn trạng thái Unresumed, nhưng điều này không thể thay đổi vì bảo đảm rằng future không làm gì trước khi được poll
  • Nếu có thể truy vấn thuộc tính của future đang được poll thì sẽ có thêm cơ hội inline tối ưu
    • Ví dụ, nếu biết future luôn trả về ready ở lần poll đầu tiên, future gọi nó sẽ không cần tạo trạng thái cho điểm await đó
    • Nếu áp dụng đệ quy kiểu tối ưu này, rất nhiều future có thể được gộp thành state machine đơn giản hơn nhiều
  • Với cấu trúc hiện tại của rustc, có vẻ mỗi async block được biến đổi riêng lẻ và dữ liệu liên quan không được giữ lại sau đó, nên không thể thực hiện các truy vấn như vậy
  • Inline future vẫn chưa được thử nghiệm, nhưng được kỳ vọng sẽ mang lại lợi ích lớn cho cả kích thước binary lẫn hiệu năng
Quảng cáo

Gộp các trạng thái giống nhau

  • Với mỗi điểm await trong async block, state machine sẽ có thêm một trạng thái
  • Mã như sau là tự nhiên, nhưng vì cả hai nhánh đều await cùng một async function nên tạo ra 2 trạng thái giống nhau
    • CommandId::A => send_response(123).await
    • CommandId::B => send_response(456).await
  • Trong trường hợp này, CoroutineLayout sẽ có _s0, _s1 lưu cùng một kiểu coroutine của send_response, đồng thời tạo hai trạng thái Suspend0, Suspend1
  • MIR của hàm này dài 456 dòng, và nhiều basic block thực chất bị lặp lại
  • Nếu refactor thủ công để trước tiên chỉ tính giá trị phản hồi, rồi chỉ gọi một lần send_response(response).await, các trạng thái trùng lặp sẽ biến mất
    • CommandId::A123
    • CommandId::B456
    • sau đó send_response(response).await
  • Sau khi refactor, CoroutineLayout chỉ còn một future được lưu và chỉ còn một trạng thái Suspend0
  • Tổng độ dài MIR giảm xuống còn 302 dòng, và sự trùng lặp biến mất
  • Vì vậy, một pass tối ưu hóa tìm các đường đi mã và trạng thái giống nhau để gộp lại có vẻ rất hữu ích
    • Tối ưu này có khả năng kết hợp rất tốt với pass inline future

Liên kết thử nghiệm và benchmark bổ sung

Kêu gọi hỗ trợ cho Project Goal

  • Công việc này đã được đệ trình dưới dạng Project Goal để thực hiện trong compiler: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
  • Nếu không có tài trợ thì rất khó đẩy được nhiều phần việc, vì vậy cần sự hỗ trợ một phần hoặc toàn phần từ các công ty hay tổ chức sẽ hưởng lợi từ công việc này
  • Thông tin liên hệ là dion@tweedegolf.com
  • Phạm vi công việc và mức tài trợ cần thiết có thể linh hoạt, nhưng ước tính €30k có thể hoàn thành toàn bộ hoặc phần lớn công việc

2 bình luận

 
GN⁺ 2026-05-06
Ý kiến trên Hacker News
  • Tôi đồng ý là tiêu đề hơi cường điệu, nhưng bài viết được viết tốt và truyền tải đúng trọng tâm
    Tôi vẫn chưa có đủ kinh nghiệm để đưa ra quan điểm mạnh về async trong Rust, nhưng có vài điểm nổi bật
    Điểm hay là có thể dùng runtime tường minh. Thay vì để cả dự án bị “nhuộm” thành async, có thể giữ mặc định là đồng bộ và chỉ dùng runtime ở các “ranh giới” vào/ra
    Cách này rất hợp với dự án tôi đang làm, và cũng khá giống chiến lược mà Zig áp dụng cho mã I/O. Trong trường hợp này, vấn đề màu sắc của hàm phần lớn cũng được giải quyết, và vì phải tách biệt nghiêm ngặt giữa mã I/O với mã thiên về CPU nên runtime I/O tường minh trở nên rất tự nhiên
    Điểm dở là toàn bộ hệ sinh thái có vẻ phụ thuộc quá nhiều vào tokio. Nó giống như trong Java, GC là tùy chọn nhưng trên thực tế ai cũng dùng cùng một runtime GC bên thứ ba, và hễ mang thư viện nào vào thì cũng bị ép theo runtime đó. Kiểu phụ thuộc trung tâm như vậy là không lành mạnh

    • Tùy bối cảnh mà có thể thấy cả hệ sinh thái phụ thuộc vào tokio, nhưng nhìn vào Rust nhúng thì sẽ dễ hiểu hơn
      Yêu cầu của runtime async trên bộ xử lý workstation và trong môi trường như RP2040 rất khác nhau. Dù vậy, vì có thể thay backend, khi viết mã I/O async cho vi điều khiển ARM M0 nhỏ thì nếu dùng embassy, một runtime tập trung vào embedded, mã nhìn gần như giống hệt mã ở môi trường khác
      Vì dùng cùng trait và interface nên có thể ít phải bận tâm tới chi tiết runtime hơn. So với việc dùng một RTOS nhỏ hay tự dựng môi trường async thì đây là một điểm khá tốt
      Những gì học được khi viết mã async với embassy cũng có thể mang sang các lĩnh vực khác
    • Tôi tò mò không biết lựa chọn thay thế là gì. Tôi hài lòng khi dùng tokio, nhưng cũng tốt nếu người khác dùng các executor khác như smol, async-std, glommio
      Dù tokio không phải một phần của thư viện chuẩn, nó vẫn được bảo trì tốt nên tình hình hiện tại có vẻ ổn. Thực ra tôi còn lo nếu nó được đưa vào thư viện chuẩn thì sẽ khó dùng executor khác hơn, và cũng khiến việc port thư viện chuẩn sang nền tảng khác khó hơn
      Tất nhiên, cũng có thể nỗi lo này là vô căn cứ
    • Nhắc đến Java thì khá thú vị, vì Java trong lịch sử cũng từng gặp vấn đề tương tự
      Logging giờ phần nào đã quy về slf4j, nhưng vẫn còn thư viện dùng thứ khác; tiện ích dùng chung ban đầu là Apache Commons, giờ thì Guava phổ biến hơn
      JSON phần nào đã ổn định quanh Jackson, nhưng Gson hay Simple-json vẫn rất phổ biến, còn annotation về khả năng null thì chưa bao giờ được chính thức hóa: từ bản phân phối không chính thức của JSR-305, qua checker framework, gần đây lại chuyển dần sang JSpecify
      Những thành phần cơ bản như vậy cần được ngôn ngữ cung cấp thì mới tránh được phân mảnh và sự bùng nổ của các thư viện chuẩn trên thực tế
    • Có rất nhiều mảng có thể dùng async mà không phụ thuộc vào tokio. Thực ra có vẻ chỉ mảng web/server mới gần như bị trói hẳn vào tokio
      Viết thư viện độc lập với executor không quá khó, nhưng đòi hỏi phải luôn cẩn thận, và điều đó không phải lúc nào cũng được phần lớn cộng đồng duy trì
  • Bài viết rất hay. Tôi thích những phân tích tối ưu hóa chuyên sâu kiểu này và hy vọng mục tiêu của dự án cũng sẽ thành công
    Tôi từng có cảm giác compiler thường không đầu tư nhiều công sức vào tối ưu hóa cho những trường hợp “tầm thường”
    Tuy vậy, tiêu đề quá kịch tính so với nội dung. Nếu là “Async Rust Optimizations the Compiler Still Misses” thì tôi vẫn sẽ bấm vào đọc

    • Tôi chọn tiêu đề như vậy đơn giản vì đó là sự thật. Từ khi async xuất hiện khoảng năm 2019 thì không có quá nhiều thứ thay đổi lớn
      Giờ có thể dùng async trong trait và closure, nhưng đó là cập nhật của hệ thống kiểu chứ không phải thay đổi của chính bộ máy async. Waker cũng dễ dùng hơn đôi chút, nhưng đó gần như là cải thiện phía std/core
      Theo hiểu biết của tôi, những người đưa async Rust vào ngôn ngữ đã bị burnout khá nhiều và ít hoạt động hơn, mà sau đó hầu như không có ai tiếp nối. Dù vậy, tôi khá mừng khi thấy phía Google có một PR tối ưu bố cục bộ nhớ cho các biến được capture
      Tôi và đồng nghiệp dùng async rất nhiều, nên có lẽ chúng tôi sẽ phải tự làm, hoặc ít nhất là tự bắt đầu. Có lẽ “miễn phí” ở đây gần với nghĩa “miễn phí như nuôi một con chó con”
      Nên đúng là tiêu đề hơi câu view, nhưng tôi vẫn không định rút lại nó
    • Tôi đồng ý là tiêu đề quá cường điệu
      Tác giả có vẻ quá ám ảnh với overhead của các hàm nhỏ. Việc khó chịu với overhead của trạng thái “panic” và “returned” không phải vấn đề lớn
      Phần lớn các khối async hữu ích đều đủ lớn để overhead của trường hợp lỗi bị chìm đi
      Còn chuyện thiếu inlining thì có thể là một điểm hợp lý. Nhưng thứ thường giới hạn số lượng hoạt động lớn chủ yếu là không gian trạng thái mà mỗi hoạt động yêu cầu
  • Nhìn chung async có vẻ là một ý tưởng còn non. Mã thông thường vốn dĩ cũng đã là bất đồng bộ rồi
    Nếu phải chờ một tác vụ async thì luồng sẽ ngủ cho đến khi sẵn sàng, và kernel trừu tượng hóa việc đó. Nhưng vì mọi người không thích tổ chức mã theo luồng logic nên đã thêm hệ thống callback cho sự kiện, rồi sau đó lại nhận ra callback khó suy luận và điều khiển tuần tự thì tốt hơn
    Vì thế tôi cho rằng luồng là mô hình lập trình đúng đắn
    Giờ các runtime ngôn ngữ lại thích “green thread” vì tính di động và hiệu năng, nhưng hầu hết ngôn ngữ không cung cấp chúng cho ra hồn. Thay vào đó lại nảy sinh các vấn đề như màu sắc async/non-async, scheduling, ưu tiên, không tiền nhiệm. Đây là mô hình tiến trình và lập lịch còn tệ hơn cả thập niên 1970

    • Câu “mã thông thường vốn đã là async, và khi chờ thì luồng ngủ còn kernel trừu tượng hóa nó” không chính xác
      Mã async cũng thường được viết theo cách không tối đa hóa được mức đồng thời có thể biểu đạt. Ví dụ thay vì “thực hiện đồng thời toàn bộ N tác vụ I/O” thì lại viết kiểu “với mỗi tác vụ X thì await process(x)”
      Nhưng trong thế giới luồng, vấn đề đồng thời này còn nghiêm trọng hơn. Bản thân luồng quá nặng nên rất khó biểu đạt đồng thời một cách hiệu quả, và cũng không có cách tối ưu nào theo hướng đó
      Đây không phải bài học mới. Từ lâu người ta đã biết executor work-stealing cho độ trễ thấp hơn rất nhiều và P99 ổn định hơn so với luồng truyền thống. Đó cũng là lý do Apple tạo ra GCD từ đầu những năm 2000
      Luồng không cung cấp cho kernel scheduler thông tin phong phú cần thiết để hiểu tải công việc, và kernel thread là cơ chế quá nặng để đạt được đồng thời tinh vi. Với tải thuần tính toán thì còn đỡ, nhưng với I/O hay tải hỗn hợp thì càng tệ hơn
      Không phải chương trình nào cũng cần mức hiệu năng này, nhưng với cùng lượng công sức, việc đạt ngưỡng hiệu năng cao hơn sẽ dễ hơn nhiều, và trên thực tế có thể đạt độ trễ và thông lượng mà cách tiếp cận truyền thống khó theo kịp
      Dấu hiệu cho thấy async đi đúng hướng cũng thể hiện ở io_uring. Cách tiếp cận I/O hiệu năng cao của kernel là io_uring hoàn toàn khác với threading và system call truyền thống, và cơ chế xử lý hoàn thành của nó cũng gần với đồng thời async hơn nhiều. Chỉ là async/await thôi thì vẫn chưa đủ “màu” để biểu đạt quan hệ giữa các tác vụ async, nên khai thác trọn vẹn còn khó hơn
    • Ngay khi kernel và OS scheduler xen vào, tốc độ có thể chậm hơn 3–4 bậc độ lớn so với mức lẽ ra đạt được
      Lần gần nhất tôi đụng vào mã coroutine/scheduling, việc tạo một luồng rồi kết thúc ngay và join mất khoảng 200µs, trong khi tự tạo green thread, lên lịch cho nó rồi chờ chỉ mất khoảng 400ns
      Không cần chờ 10 năm cho đến khi ai đó lại thiết kế thêm một framework async phi lý và quá phức tạp nữa. Trong bất kỳ ngôn ngữ hệ thống nào, chỉ cần 20 dòng assembly là có thể tự làm green thread/coroutine có stack
    • Việc “luồng là mô hình lập trình đúng đắn” hay không còn tùy bạn đang làm gì. Với tác vụ thiên về tính toán thì luồng phù hợp, còn với tác vụ thiên về băng thông thì async phù hợp
      Tối ưu mã thiên về băng thông là vấn đề thiết kế lịch chạy. Trong mô hình đa luồng cổ điển, khả năng kiểm soát scheduling rất hạn chế, còn trong mô hình async thì gần như kiểm soát được hoàn toàn
      Một lịch chạy async được tối ưu tốt sẽ nhanh hơn rất nhiều so với kiến trúc đa luồng tương đương cho cùng kiểu tác vụ thiên về băng thông, đến mức khó so sánh
      Phần lớn mã hiệu năng cao ngày nay thiên về băng thông, và async tồn tại để việc tối ưu loại tải này trở nên dễ hơn
    • Tôi lại thấy callback dễ suy luận hơn
      Khi kiểm thử xử lý đồng thời và xác nhận có xử lý đúng race condition hay không, callback dễ hơn rất nhiều vì có thể kiểm soát scheduling. Mỗi callback đại diện cho một đơn vị tách biệt, nên có thể thấy được những sự kiện nào có thể bị đảo thứ tự và dễ xem xét nhiều thứ tự khác nhau hơn
      Ngược lại, với luồng thì rất dễ bỏ qua thứ tự, và cũng dễ không nghĩ tới chuyện sự phức tạp phát sinh ở luồng khác sẽ ảnh hưởng tới luồng hiện tại khi nào. Nó không hẳn là đơn giản, mà giống như đơn giản hóa quá mức
      Ngoài ra, trừ khi chèn các rào cản nhân tạo để chặn luồng, hoặc truyền vào mock có callback với I/O đã được thay bằng stub để kiểm soát thứ tự, thì rất khó thực sự thay đổi kịch bản đồng thời khi kiểm thử
      Vấn đề của callback là stack trace được capture không phải stack trace logic. Nếu không có thư viện/runtime đã bỏ công làm cho stack trace có ý nghĩa hơn thì cần có định nghĩa lỗi tốt
      Tất nhiên cũng có thể trộn cả hai mô hình và nhận về đúng nhược điểm của cả hai
    • Luồng không tốt hơn hay tệ hơn async+callback, mà là một mô hình khác. Có những bài toán rất hợp với luồng, và cũng có những bài toán biểu đạt bằng async tốt hơn nhiều
  • Nếu mục tiêu chính của Rust là an toàn thì tôi không hiểu vì sao lại có panic. Phải có cách chứng minh rằng trong mã không tồn tại đường đi nào có thể panic
    Tôi đã xem xét chuyện này cả tuần nay, và thấy việc tạo ra một chương trình bảo đảm tuyệt đối không panic là cực kỳ khó. Theo tôi hiểu thì panic handler nặng khoảng 300KB, và cách duy nhất để loại nó ra là tại thời điểm biên dịch mã không được có bất kỳ đường đi nào có thể panic. Việc kiểm tra sau biên dịch xem panic handler có bị đưa vào binary hay không nghe như một mẹo vá víu
    Có thể chặn unwrap và các thao tác panic khác bằng lint, nhưng nếu tồn tại một tập con no-panic của Rust thì phần lớn vấn đề mà bài này nói tới đã biến mất
    Thật khó chịu khi phải làm việc với một ngôn ngữ có quá nhiều phép toán về mặt lý thuyết có thể panic, dù trên thực tế chỉ xảy ra ở mức bit flip hay thứ gì tương tự. Chuyện này cũng giống khi phải chứng minh mảng không rỗng, hay khi làm việc với async
    Rốt cuộc người ta phải thêm hàng đống xử lý lỗi cho những tình huống gần như sẽ không bao giờ xảy ra, hoặc dùng những cấu trúc kỳ quặc như mẫu danh sách không rỗng với trường đầu tiên tách riêng khỏi phần còn lại. Và bản thân cấu trúc đó cũng làm phình mã thêm

    • Phía Rust-in-Linux đang xử lý vấn đề này bằng những thứ như thao tác cấp phát bộ nhớ có thể thất bại. Với họ đây là tính năng cần thiết
      Công việc mở rộng cách dùng dựa trên chứng minh, bao gồm cả chứng minh rằng mảng không rỗng, cũng đang tiến triển chậm mà chắc
    • panic khá quan trọng cho tính dễ dùng và an toàn
      Nếu không có panic và phải tiếp tục thực thi trong mọi tình huống, thì trong các trường hợp như bộ nhớ bị hỏng làm vỡ invariant, bạn sẽ phải thêm rất nhiều xử lý lỗi ở mọi chỗ kiểm tra invariant chỉ để cố phục hồi
      Điều đó chính là cùng một loại vấn đề mà bạn đang lo ngại: một đống xử lý lỗi khổng lồ cho những tình huống gần như chẳng bao giờ xảy ra
    • Mục tiêu của Rust là an toàn bộ nhớ. Xét trên phương diện an toàn bộ nhớ thì panic hoàn toàn an toàn
    • Ngay cả hệ điều hành chạy chương trình của bạn cũng không hoàn hảo
      Tôi khá mệt với thái độ chỉ muốn công cụ làm cho mọi thứ trở nên không thể thất bại mà bản thân thì không muốn tự làm gì cả. Người ta muốn API thật dễ, nếu vẫn chưa đủ dễ thì muốn container Kubernetes được “lập trình” bằng YAML, và nếu vậy vẫn chưa đủ dễ thì lại muốn dịch vụ hosting kiểu bấm chọn của GCP hay Amazon
      Cuối cùng đó không còn là muốn lập trình nữa mà gần như là muốn tiêu thụ các ứng dụng không bao giờ hỏng, trong khi lối sống đó chỉ tồn tại nhờ mối quan hệ cộng sinh với những người thực sự xây ra mọi thứ
  • Những cuộc thảo luận xấu xí nhưng cần thiết như thế này cũng đã diễn ra bên C++ một thời gian
    Từ khi async được đưa vào Rust, tôi đã không thích bản chất lây lan của nó
    Tôi hy vọng Rust sẽ thành công, và nếu có nhiều người như thế này hơn thì tương lai của Rust có thể sẽ sáng sủa hơn

  • Gần đây tôi bắt đầu làm việc với async trong Rust, và vấn đề chính tôi đang gặp là trùng lặp mã
    Bất kỳ hàm nào muốn hỗ trợ cả API bất đồng bộ lẫn API blocking đều phải viết trùng lặp. Có lẽ maybe-async sẽ rất hay
    Tôi đã thử né chuyện này bằng cách xem qua các crate như maybe-async, bisync, nhưng cái nào cũng có vấn đề hoặc ràng buộc nặng

    • Hiện đang có công việc về generic theo từ khóa để có thể biến hàm thành generic theo các từ khóa như async hay const
      Hiện tại, lựa chọn tốt nhất để viết mã có thể sống ở cả hai phía đồng bộ/bất đồng bộ là sans-io. Thomas Eizinger của Fireguard có một bài viết rất hay về mẫu này[1]
      Mẫu này không chỉ giải quyết gọn bài toán sync/async mà còn giúp việc kiểm thử dễ hơn, đồng thời mở ra cánh cửa cho các kỹ thuật như DST[2]
      Tôi cũng có viết một bài về chủ đề này[3], trong đó nhấn mạnh đây là một vấn đề rộng hơn chuyện async so với sync, vì còn bao gồm cả các executor khác nhau
      0: https://github.com/rust-lang/effects-initiative
      1: https://www.firezone.dev/blog/sans-io
      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
      3: https://hugotunius.se/2024/03/08/on-async-rust.html
    • Điều này thực sự phụ thuộc rất nhiều vào việc bạn đang làm gì, nhưng nếu đủ đơn giản thì có thể tự tạo một macro để hoán đổi kiểu và await
    • Đây là vấn đề màu sắc của hàm kinh điển. https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • Theo góc nhìn của tôi, hàm async vốn đã là maybe-async rồi
      Khác biệt giữa fn -> voidfn -> Future là cái đầu chạy tới cuối ngay lập tức, còn cái sau có thể chỉ hoàn tất vào lúc nào đó sau này
      Nếu muốn chạy một hàm async theo kiểu blocking thì chỉ cần dùng executor blocking
  • Điều tôi thích ở bài viết này là nó còn cho thấy cả mục tiêu Rust 2026
    Nhóm tôi có dùng Rust, nhưng chưa cần đào quá sâu để làm được việc mình cần. Dù vậy, vẫn rất thú vị khi nhìn một ngôn ngữ phát triển từ nền móng lên với rất nhiều phản hồi từ cộng đồng
    Tôi không cảm nhận được điều đó nhiều ở C++, và cũng không rõ trong các lĩnh vực khác thì mọi thứ diễn ra thế nào
    Chỉ có điều hơi tiếc là mỗi mục tiêu dường như lại cần nguồn tài trợ riêng nên hơi giống Kickstarter. Tôi tự hỏi liệu đây có phải mô hình tốt nhất mà hiện giờ người ta tìm ra hay chưa

    • Thuật ngữ “mục tiêu dự án” thực ra dễ gây hiểu lầm hơn so với ý nghĩa thật của nó
      Mục tiêu dự án là một hệ thống để một cá nhân hoặc nhóm nhỏ nói rằng họ muốn làm một việc nào đó, rồi xin những người tình nguyện của dự án Rust hỗ trợ lâu dài như review mã hoặc trả lời câu hỏi
      Điều đó không có nghĩa bản thân dự án Rust đã đặt ra mục tiêu đó, hay nhất thiết ủng hộ nó
      Vì thế xem đây là roadmap chính thức của Rust là không đúng; chính xác hơn nên hiểu nó là “có những người đóng góp muốn làm việc trong mảng này”
    • Ngay trong ủy ban ISO C++ dường như cũng có sự đồng thuận ở mức nào đó rằng quá trình tiến hóa của ngôn ngữ này đã bị hỏng. Chủ yếu là vì quy mô và cách tổ chức
      Khi một công nghệ đã đứng vững về mặt thương mại thì thật đáng tiếc là mọi thứ dường như thường trôi theo hướng này. Khó mà trách các nhà tài trợ lớn khi họ chỉ tài trợ cho những phần mà họ quan tâm
      May là theo tôi biết thì một phần đáng kể nguồn tiền của TweedeGolf đến từ chính phủ Hà Lan
    • Có vẻ trong công việc mã nguồn mở có đại khái hai loại: phát triển tính năng và bảo trì
      Tính năng mới thì có thể “bán” được. Làm ra nó cần tiền, nhưng nó giải quyết vấn đề thật, và nếu chi phí của vấn đề lớn hơn chi phí phát triển tính năng thì doanh nghiệp thường sẵn lòng trả
      Bảo trì thì khó hơn, nhưng giờ cũng đã có các quỹ cho maintainer. Quỹ của RustNL là một ví dụ: https://rustnl.org/maintainers/
      Những quỹ như vậy nhắm tới công việc rộng hơn và bền vững hơn, được hậu thuẫn bởi nhiều tổ chức cùng đóng góp một ít
      Tôi không biết đó có phải mô hình tốt nhất hay không, nhưng ít nhất có vẻ nó hoạt động ở mức nào đó
  • Nếu đọc tài liệu về Rust Async và Tokio, sẽ thấy có giải thích rất rõ vì sao không nên nhét phần CPU-intensive vào ngăn xếp async, cách dùng hiệu quả các công cụ cơ bản như std::sync::Mutex trong khối async, và cách nối mã đồng bộ với mã async
    Nhiều mã không quan tâm hoặc không cần hiệu quả nên không tuân theo các hướng dẫn này. Nhưng cũng có nhiều dự án coi trọng hiệu năng và hiệu suất, và khi mã chạy trong production thì người ta sẽ nhận ra các cái bẫy. ScyllaDB là một ví dụ
    LLM cũng không giúp được. Chúng tạo ra mọi thứ theo kiểu async tận tới main, dùng nhầm công cụ mặc định, và không thiết kế hệ thống cho đúng

  • Việc gộp các trạng thái trùng lặp, tức mẫu kéo match ra ngoài nhánh await như trong ví dụ process_command, là cách dễ nhất mà bất kỳ ai cũng có thể áp dụng cho mã async hiện có ngay hôm nay
    Không cần làm gì ở compiler, chỉ cần refactor thôi

    • Ít nhất cũng cần một lint tùy chỉnh để tìm ra chỗ nào có thể áp dụng. Chừng đó thì cũng đã khá gần với công việc ở compiler rồi
  • Về đoạn “Future không dễ được inline”, trong ngôn ngữ lập trình tôi tự làm, tôi đã viết một pass tùy chỉnh để inline lời gọi hàm async bên trong hàm async
    Nhìn chung nó hoạt động tốt và có thể loại bỏ một phần boilerplate, nhưng kích thước binary kết quả tăng lên đáng kể
    Về mặt kỹ thuật, Rust cũng có thể làm điều tương tự

 
GN⁺ 2026-05-06
Ý kiến trên Lobste.rs
  • Đây là một bài viết mang tính xây dựng hơn tôi tưởng rất nhiều khi chỉ nhìn tiêu đề

    • Tôi nghĩ nó khá sát với sự thật. Đã 7 năm kể từ khi phát hành MVP, nhưng hầu như không có tiến triển nào đáng kể trong thiết kế ngôn ngữ hay triển khai trình biên dịch, và những người chủ yếu tạo ra MVP cũng giảm mức độ tham gia dự án vào khoảng cùng thời điểm, khiến việc chuyển giao sau đó gần như bị đình trệ
      Mong rằng những ai muốn làm việc này sẽ nhận được sự hỗ trợ cần thiết
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    Thật tốt khi thấy vấn đề này đang được xử lý. Tôi đã vài lần đọc những bài viết nói rằng hiện tại rustc đang chuyển quá nhiều mã sang LLVM và chỉ mong trình tối ưu hóa xử lý hết, đặc biệt bài này còn đang kêu gọi tài trợ cho công việc đó

  • Trời ạ, hóa ra tôi đã ngốc thật
    Tôi luôn nghĩ async về bản chất sẽ luôn “cồng kềnh” ở một mức nào đó vì nó cần runtime, theo dõi tác vụ và polling để xác nhận hoàn thành. Phần overhead đó đâu thể bằng 0
    Tôi vẫn cho rằng “trừu tượng không chi phí” ở đây là nói về tính năng ngôn ngữ, tách biệt với runtime được thêm vào
    Tôi thậm chí chưa từng nghĩ đến việc xem rustc thực sự tạo ra gì trước khi chuyển cho LLVM

  • Dành cho những ai chưa quen với async Rust:

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    Điều này thực sự đúng. Ngay cả một cây lồng nhiều lời gọi async, sau tối ưu hóa tối đa, cũng sẽ được cô đặc thành một struct duy nhất với máy trạng thái bên trong. Cách làm này thực sự rất thông minh

  • Nếu đi tới trường hợp này trong bản build phát hành thì có tạo ra một kiểu deadlock không? Hay cũng có thể gây rò rỉ do các tác vụ cứ chờ những công việc luôn ở trạng thái Pending?

    • Đúng vậy. Những future như thế sẽ rơi vào trạng thái bị kẹt và sẽ không bao giờ hoàn thành. Tuy nhiên, trạng thái đó chỉ có thể đạt tới trong mã async mức thấp vốn đã có lỗi, và đoạn mã không theo dõi đúng future đã hoàn thành thì rất có thể vốn dĩ cũng đã gây rò rỉ và deadlock rồi
      Không thể poll sai bằng .await
  • Tôi có vài suy nghĩ:

    1. Bài này có vẻ đang lập luận rằng nên đưa thêm nhiều logic tối ưu hóa ra khỏi LLVM và chuyển lên tầng MIR. Ví dụ, tôi hiểu vì sao việc inline hàm async ở MIR lại dễ hơn ở LLVM. Nếu đã làm được việc đó cho async trong MIR, thì liệu có thể khái quát hóa logic đó cho cả hàm đồng bộ và loại bỏ một phần các optimization pass của LLVM không? Tôi biết đây là một việc lớn, và đây gần như là câu hỏi về định hướng hơn là câu hỏi thực dụng. Có vẻ như khi trình biên dịch frontend/middle-end đủ phức tạp, sẽ hợp lý hơn nếu chuyển một phần đáng kể các tối ưu hóa tổng quát của LLVM sang nơi khác
    2. Tôi vẫn không thích panic=unwind. Ngoài một số test harness, tôi gần như chưa thấy lợi ích nào của nó đủ để bù lại chi phí so với panic=abort. Ngay cả test harness, ít nhất trên Linux, có lẽ cũng có thể áp dụng lựa chọn tương tự bằng cách dùng clone theo kiểu khá khó hiểu để wait trên thread thực thi thay vì pthread_join. Có thể tôi sai ở điểm này
  • Có ai khác cũng vừa thấy link bị hỏng không?
    Sửa: bài blog hiện lên khoảng nửa giây rồi chuyển sang trang 404
    Sửa 2: Tôi vào danh sách bài viết của blog rồi bấm thử vài thứ, và ngay cả khi mở đúng bài đó trong danh sách cũng vẫn ra trang 404. Làm sao người ta có thể phá hỏng một blog là trang tĩnh, hoặc ít nhất đáng ra phải là vậy, theo cách này được chứ?

    • Giọng điệu này có vẻ hơi thô lỗ và công kích một cách không cần thiết. Website cũng có thể gặp lỗi, và việc báo lỗi là hữu ích, nhưng bình luận này nghe khá cáu kỉnh
      Tham khảo thêm, tôi đã thử làm theo đúng các bước tái hiện có vẻ giống vậy nhưng hoàn toàn không gặp 404. Tôi thử trên cả điện thoại lẫn máy tính, với JavaScript bật và tắt. Vì thế có thể hiện tượng họ gặp phức tạp hơn vẻ bề ngoài