Trong thế giới phát triển phần mềm, có rất ít chủ đề tạo ra nhiều cuộc thảo luận sôi nổi như việc lựa chọn giữa lập trình async/await và lập trình đa luồng truyền thống. Trong khi một bài viết kỹ thuật gần đây khám phá các tác động về hiệu suất của cả hai phương pháp, thì cộng đồng nhà phát triển vẫn đang tích cực tranh luận về những câu hỏi kiến trúc sâu hơn về cách chúng ta xử lý các hoạt động đồng thời trong các ứng dụng hiện đại.
Sự Khác Biệt Cốt Lõi Giữa Tính Đồng Thời Tường Minh và Ngầm Định
Sự chia rẽ cơ bản trong cuộc tranh luận này xoay quanh việc nhà phát triển nên có bao nhiêu quyền kiểm soát đối với các hoạt động đồng thời. Async/await làm cho tính đồng thời trở nên tường minh - mọi điểm tạm dừng đều được đánh dấu bằng từ khóa await, tạo ra thứ mà một số người gọi là các hàm có màu (colored functions) hoạt động khác với mã đồng bộ. Tính tường minh này đi kèm với chi phí nhận thức nhưng mang lại cho nhà phát triển khả năng kiểm soát chi tiết về thời điểm các hoạt động nhường quyền điều khiển.
Các triển khai green thread như goroutines trong Go hoặc các ngôn ngữ BEAM (Erlang, Elixir) lại tiếp cận theo hướng ngược lại. Chúng làm cho tính đồng thời gần như vô hình ở cấp độ mã, dựa vào các hệ thống thời gian chạy tinh vi để quản lý lập lịch và chuyển đổi ngữ cảnh. Như một bình luận viên đã nhận xét về các ngôn ngữ BEAM: Bạn không có sự rối rắm khó xử này giữa các từ khóa dành riêng và vòng lặp sự kiện. Nếu bạn muốn một đoạn mã khác được thực hiện sau này vì một sự kiện, bạn chỉ cần sắp xếp để một sự kiện như vậy được gửi đến.
Không có callback. Không có async coloring. Chỉ có sự kiện. Giải pháp cho vấn đề sự kiện là tập trung và làm cho vòng lặp sự kiện của bạn có thể sử dụng được một cách tổng quát hơn.
Sự Đánh Đổi Hiệu Suất Vượt Ra Ngoài Các Phép Đo Lường Đơn Giản
Mặc dù các cuộc thảo luận ban đầu thường tập trung vào thông lượng thô, nhưng cuộc tranh luận trong cộng đồng đã tiết lộ những cân nhắc về hiệu suất tinh tế hơn. Các triển khai Async/await thường sử dụng các state machine chỉ lưu các biến cần thiết trong thời gian tạm dừng, khiến chúng hiệu quả về bộ nhớ cho các khối lượng công việc ràng buộc I/O. Tuy nhiên, green thread duy trì stack đầy đủ cho mỗi tác vụ đồng thời, điều này có thể tốn bộ nhớ hơn nhưng tránh được việc xé stack (stack ripping) mà các state machine async yêu cầu.
Cuộc trò chuyện về hiệu suất đã phát triển để thừa nhận rằng cả hai phương pháp tiếp cận đều đã vượt ra ngoài những hạn chế ban đầu của chúng. .NET đang phát triển Runtime Async để thay thế các state machine tường minh bằng các cơ chế tạm dừng thời gian chạy, trong khi Go đã cải thiện quản lý stack của mình từ các stack phân đoạn sang sao chép stack. Như một nhà phát triển nhận xét, Cả green thread và async/await đều tốn kém hơn đáng kể so với mã đơn luồng, nhưng chi phí của chúng thể hiện theo những cách khác nhau.
Sự Khác Biệt Chính Giữa Async/Await và Green Threads
| Khía cạnh | Async/Await | Green Threads |
|---|---|---|
| Kiểm soát | Tường minh (từ khóa await) | Ngầm định (quản lý bởi runtime) |
| Sử dụng bộ nhớ | State machines (chỉ lưu các biến cần thiết) | Full stacks cho mỗi task |
| Hệ sinh thái | Phổ biến rộng rãi (Python, JS, C, Rust) | Đặc thù theo ngôn ngữ (Go, BEAM) |
| Phân tán | Yêu cầu thêm các framework | Tích hợp sẵn (ngôn ngữ BEAM) |
| Độ khó học | Khó hơn do "colored functions" | Dễ hơn cho các trường hợp đơn giản |
| Đặc điểm hiệu năng | Tốt hơn cho khối lượng công việc I/O-bound | Tốt hơn cho một số mô hình đồng thời nhất định |
Sự Phân Chia Về Hệ Sinh Thái và Công Cụ
Có lẽ cân nhắc thực tế nhất nổi lên từ các cuộc thảo luận cộng đồng là sự trưởng thành của hệ sinh thái xung quanh mỗi phương pháp tiếp cận. Async/await đã trở nên phổ biến khắp Python, JavaScript, C# và Rust, tạo ra một kho tàng các thư viện tương thích và các mẫu hình đã được thiết lập. Tuy nhiên, điều này đi kèm với vấn đề tô màu hàm (function coloring problem) nơi mã async và mã đồng bộ thường không trộn lẫn một cách liền mạch.
Các hệ sinh thái green thread, đặc biệt là các ngôn ngữ BEAM, cung cấp khả năng tính toán phân tán vốn được xây dựng cơ bản trong thiết kế của chúng. Như một bình luận viên giải thích: BEAM được tạo ra cho tính toán phân tán. Bạn có thể sinh ra các tiến trình mới, giao tiếp giữa các tiến trình (mà không cần phải ở trên cùng một máy tính), gửi bất kỳ loại dữ liệu nào giữa chúng bao gồm cả closures. Khả năng phân tán được tích hợp sẵn này đại diện cho một lợi thế kiến trúc đáng kể cho một số trường hợp sử dụng cụ thể.
Các Triển Khai Đáng Chú Ý Theo Ngôn Ngữ
- Async/Await: asyncio của Python, async/await của JavaScript, async của C, async/await của Rust
- Green Threads: goroutines của Go, các ngôn ngữ BEAM (processes của Erlang/Elixir), Project Loom của Java
- Các Phương Pháp Kết Hợp: Runtime Async sắp ra mắt của .NET, nhiều tùy chọn runtime của Rust
Công Cụ Phù Hợp Cho Công Việc Phù Hợp
Sự đồng thuận nổi lên từ các cuộc thảo luận của nhà phát triển cho thấy rằng sự lựa chọn giữa các mô hình này phụ thuộc nhiều vào các yêu cầu ứng dụng cụ thể. Async/await xuất sắc trong các môi trường hạn chế tài nguyên và lập trình hệ thống, trong khi green thread mang lại tính công thái học nhà phát triển vượt trội cho các ứng dụng kinh doanh và hệ thống phân tán.
Nhiều nhà phát triển hiện đang áp dụng cách tiếp cận kết hợp, sử dụng các runtime async đơn luồng cho các hoạt động quản lý trong khi sử dụng các giải pháp đa luồng cho công việc quan trọng về hiệu suất. Như một người thực hành chia sẻ: Trong công việc của tôi với mã phía máy chủ, tôi sử dụng nhiều runtime async. Một runtime là đa luồng và xử lý tất cả lưu lượng thực. Một runtime là đơn luồng và xử lý các hoạt động quản lý như gửi số liệu và nhật ký.
Cuộc tranh luận tiếp tục phát triển khi các triển khai mới xuất hiện. Rust hiện có các runtime async như Glommio và Monoio được xây dựng trên io_uring, trong khi những cải tiến sắp tới của .NET hứa hẹn sẽ thu hẹp khoảng cách hiệu suất giữa mã async và mã đồng bộ. Điều rõ ràng là cả hai phương pháp tiếp cận sẽ tiếp tục cùng tồn tại, mỗi phương pháp giải quyết các vấn đề khác nhau trong bối cảnh phức tạp của phát triển phần mềm hiện đại.
Cuộc trò chuyện xung quanh async so với threads không còn là về việc phương pháp tiếp cận nào tốt hơn một cách phổ quát, mà là về việc hiểu các sự đánh đổi và lựa chọn công cụ phù hợp cho các trường hợp sử dụng cụ thể. Khi các ngôn ngữ lập trình tiếp tục phát triển, chúng ta đang thấy sự hội tụ hơn là phân kỳ, với mỗi hệ sinh thái mượn những ý tưởng thành công từ những hệ sinh thái khác.
Tham khảo: [Quite] A Few Words About Async
