Tin tức

Async/Await Là Cái Bẫy: Tại Sao Rust Cần Thread-Per-Core

Async/await che giấu độ phức tạp nhưng thất bại trong sản xuất. Một framework Rust mới, Tina, áp dụng cách tiếp cận thread-per-core để khắc phục.

July 16, 2026· 4 min read
Async/Await Là Cái Bẫy: Tại Sao Rust Cần Thread-Per-Core

Async/await đã thắng trong cuộc chiến về đồng thời vì nó dễ viết. Nhưng như Rich Hickey và Rob Pike đã chỉ ra, dễ không có nghĩa là đơn giản. Bên dưới lớp vỏ, async/await che giấu các máy trạng thái, làm mờ thực tế phần cứng, và đẩy độ phức tạp lập lịch lên vai lập trình viên. Trong sản xuất, sự trừu tượng này thường sụp đổ một cách thảm khốc.

Vấn Đề Cốt Lõi: Bất Đồng Bộ vs. Đồng Thời

Async/await gộp nhầm bất đồng bộ (nhường CPU khi I/O) với đồng thời (xử lý nhiều việc cùng lúc). Một lập trình viên viết một hàm async trông như mã tuần tự, nhưng nếu hàm đó thực hiện một tác vụ tốn CPU 50ms—phân tích một tải JSON lớn hoặc chạy một bằng chứng mật mã—thì bộ điều phối cộng tác bị kẹt. Trong các runtime như Tokio hay Node.js, thread không nhường CPU cho đến khi gặp điểm await. Một tác vụ tính toán nặng có thể làm tăng độ trễ cho hàng nghìn yêu cầu không liên quan và khiến hệ thống không phản hồi.

Bộ Điều Phối Có Con Người Trong Vòng Lặp

Khi các đợt tăng độ trễ này xảy ra, giải pháp tiêu chuẩn là tách các runtime: dùng Tokio cho I/O và Rayon cho tác vụ CPU. Nhưng điều này buộc lập trình viên phải tự tay phân chia mọi hàm vào pool I/O hay tính toán, điều phối truyền thông điệp giữa chúng, và kiểm soát ranh giới để tránh deadlock. Như các báo cáo hậu sự cố từ PostHog và Meilisearch cho thấy, điều này biến lập trình viên thành một bộ điều phối con người—chính xác những gì async/await được cho là sẽ loại bỏ. Nếu bạn phải tự quản lý hai runtime với các mô hình tư duy khác nhau, thì sự trừu tượng đã thất bại.

Không Giới Hạn Mặc Định Là OOM Mặc Định

Gọi tokio::spawn(...) rất rẻ, điều này nguy hiểm. Khi một cơ sở dữ liệu hạ nguồn chậm lại trong lúc tăng đột biến lưu lượng, vòng lặp ingress tiếp tục chấp nhận kết nối và sinh ra các tác vụ. Các tác vụ async và cấp phát bộ nhớ thường không bị giới hạn, vì vậy hệ thống không có cơ chế đẩy lùi. Các tác vụ đang chờ xếp hàng vô thời hạn cho đến khi OOM killer kết thúc tiến trình. Hàng đợi không sửa được quá tải; chúng chỉ trì hoãn sự cố và làm nó trở nên thảm khốc hơn.

Huyền Thoại Work-Stealing

Khi hệ thống gặp nút thắt cổ chai, lập trình viên thường yêu cầu bộ điều phối work-stealing để đảm bảo công bằng. Nhưng ở quy mô lớn, công bằng phá hủy thông lượng. Work-stealing di chuyển các máy trạng thái giữa các lõi CPU, từ bỏ bộ nhớ đệm L1/L2 và chịu chi phí truy xuất bộ nhớ chính hơn 100 nano giây. Như WhatsApp đã phát hiện với Erlang BEAM trên máy 100+ lõi, các thread nhàn rỗi tranh giành khóa hàng đợi chạy toàn cục có thể làm nghẹt hệ thống. Nếu bạn đã buộc phải tự phân chia thread cho I/O và CPU, thì thuật toán work-stealing chung chung đã thất bại với bạn.

Giải Pháp Thay Thế: Thread-Per-Core

Hãy đến với Dự án Tina, một framework đồng thời cho Rust theo phong cách shared-nothing, thread-per-core, có tính chủ kiến. Thay vì che giấu máy trạng thái, Tina phơi bày nó và cung cấp cho lập trình viên các nguyên thủy kiểm soát tốt hơn. Mỗi lõi chạy một thread hệ điều hành với vòng lặp điều phối riêng và các vùng cách ly (ví dụ: kết nối TCP, trình xử lý HTTP, worker). Không có work-stealing, không có mutex, và có tính cục bộ bộ nhớ đệm nghiêm ngặt. Giao tiếp giữa các shard diễn ra qua các vòng SPSC không khóa. Đó là sự trở lại với khả năng chịu lỗi của BEAM mà không có garbage collection mờ đục và work-stealing toàn cục.

Tina chấp nhận các ràng buộc nghiêm ngặt để đảm bảo thông lượng và độ tin cậy khổng lồ. Đây là lời nhắc nhở rằng đôi khi kiến trúc đơn giản nhất—một thread trên mỗi lõi, không chia sẻ bộ nhớ—lại là kiến trúc mạnh mẽ nhất.