Async Rust chưa bao giờ vượt qua trạng thái MVP
(tweedegolf.nl)- 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áiUnresumed,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::Pendingthay 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
- Đây là vấn đề đã được biết đến, và đã có PR xử lý một phần: https://github.com/rust-lang/rust/pull/135527
Cấu trúc của future được tạo ra
- Mã ví dụ có
foo()trả vềasync { 5 }, cònbar()thực hiệnfoo().await + foo().await- Ví dụ Godbolt: godbolt
barcó 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_resumelà 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
bartạo ra 360 dòng MIR, trong khi phiên bản đồng bộ chỉ dùng 23 dòng CoroutineLayoutmà compiler in ra về bản chất là một tập trạng thái dưới dạng enumUnresumed: trạng thái ban đầuReturned: trạng thái đã hoàn tấtPanicked: trạng thái sau panicSuspend0: điểm await đầu tiên, nơi lưu future củafooSuspend1: điểm await thứ hai, nơi lưu kết quả đầu tiên và futurefoothứ hai
Future::polllà 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ềReadyrồi chuyển future sang trạng tháiReturned - Nếu poll lại ở trạng thái này thì sẽ xảy ra panic
- Hiện tại sau
- Trạng thái
Panickeddườ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ởicatch_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
Panickedchỉ ở 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
Returnedhiệ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ểuFuturemà 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 = falsecho 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áiPanicked, nhưng tác động của việc này cần được xem xét thêm
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
footrả về 5, nhưng không thể tối ưu đáp án củabarthành 10 - Lời gọi hàm poll của
foovẫ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
footrên thực tế chỉ được gọi một lần và không panic
- Trong assembly được tạo ra, LLVM biết rằng
- 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
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ệnfoo(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ủafoo - Hiệu quả hơn là để
barchính là future củafoo
- Trường hợp có preamble và postamble thì phức tạp hơn
- Ví dụ:
bar(input)tạoblahtừinput > 10, sau đófoo(blah).awaitrồi áp dụng* 2lê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
- Ví dụ:
- Dạng
barnà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
foocapture - Tuy vậy
barkhông thể đơn giản trở thành chínhfoo, mà phần lớn trạng thái có thể dựa vàofoo
- Không có dữ liệu nào cần được giữ qua điểm await duy nhất ngoài giá trị đã được
- Nếu tự cài đặt bằng tay,
BarFutcó thể có các trạng tháiUnresumed { input }và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áiInlined - Sau đó áp dụng postamble lên kết quả của
foo.poll(cx)
- Ở lần poll đầu tiên, nó chạy preamble để tạo
- 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
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).awaitCommandId::B => send_response(456).await
- Trong trường hợp này,
CoroutineLayoutsẽ có_s0,_s1lưu cùng một kiểu coroutine củasend_response, đồng thời tạo hai trạng tháiSuspend0,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ấtCommandId::Alà123CommandId::Blà456- sau đó
send_response(response).await
- Sau khi refactor,
CoroutineLayoutchỉ còn một future được lưu và chỉ còn một trạng tháiSuspend0 - 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
- Khi áp dụng đồng thời cả hai thử nghiệm, benchmark tổng hợp x86 dùng executor
smolcho thấy hiệu năng tăng khoảng 3% - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
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
Ý 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
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
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ứ
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ế
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
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á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
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
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
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
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
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
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
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
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-asyncsẽ rất hayTô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
asynchayconstHiệ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
asyncvốn đã làmaybe-asyncrồiKhác biệt giữa
fn -> voidvàfn -> Futurelà 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àyNế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
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”
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
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::Mutextrong khối async, và cách nối mã đồng bộ với mã asyncNhiề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 đúngViệc gộp các trạng thái trùng lặp, tức mẫu kéo
matchra 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 nayKhông cần làm gì ở compiler, chỉ cần refactor thô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ự
Ý 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 đề
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
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:
Đ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?Không thể poll sai bằng
.awaitTôi có vài suy nghĩ:
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ớipanic=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ùngclonetheo kiểu khá khó hiểu đểwaittrên thread thực thi thay vìpthread_join. Có thể tôi sai ở điểm nàyCó 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ứ?
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