- Tác giả trong quá trình học ngôn ngữ Zig và thực hiện dự án viết lại chỉ mục AcoustID đã thử một hướng tiếp cận mới sau khi chạm tới giới hạn của lập trình mạng
- Để triển khai mô hình I/O bất đồng bộ và đồng thời từng dùng trong C++ và Go ngay trong Zig, tác giả quyết định tự phát triển một thư viện riêng
- Kết quả là thư viện Zio hiện thực mô hình đồng thời kiểu Go theo cách phù hợp với Zig, cho phép viết mã bất đồng bộ trông như mã đồng bộ mà không cần callback
- Zio hỗ trợ I/O mạng và tệp bất đồng bộ, channel, primitive đồng bộ, giám sát tín hiệu và cho hiệu năng ở chế độ đơn luồng nhanh hơn Go hoặc Tokio của Rust
- Dự án này cho thấy khả năng kết hợp giữa hiệu năng cấp hệ thống của Zig và mô hình đồng thời hiện đại, và được xem là một bước ngoặt quan trọng trong việc mở rộng hệ sinh thái Zig
Zig và động lực ban đầu
- Ban đầu tác giả chỉ theo dõi Zig, một ngôn ngữ được thiết kế cho các ngôn ngữ cấp thấp dành cho phần mềm âm thanh, nhưng chưa cảm thấy có nhu cầu thực tế phải dùng
- Sau khi thấy Andrew Kelley, người tạo ra Zig, tái hiện thực thuật toán Chromaprint của tác giả bằng Zig, tác giả bắt đầu quan tâm hơn
- Tác giả lấy dự án viết lại inverted index của AcoustID làm cơ hội để học Zig, và cuối cùng đạt được một bản hiện thực nhanh hơn và có khả năng mở rộng tốt hơn phiên bản C++
- Tuy nhiên, ở giai đoạn thêm giao diện máy chủ, tác giả gặp phải vấn đề thiếu hỗ trợ mạng bất đồng bộ
Cách tiếp cận cũ và giới hạn
- Ở phiên bản C++ trước đó, tác giả dùng framework Qt để xử lý I/O bất đồng bộ; tuy dựa trên callback nhưng vẫn khả dụng nhờ hệ hỗ trợ phong phú
- Trong các nguyên mẫu sau này, tác giả tận dụng sự tiện lợi về mạng và đồng thời của ngôn ngữ Go, nhưng trong Zig lại thiếu mức trừu tượng tương tự
- Để hiện thực máy chủ TCP và tầng cluster bằng Zig, tác giả buộc phải tạo ra rất nhiều thread, dẫn tới kém hiệu quả
- Để giải quyết điều này, tác giả trực tiếp viết client Zig cho hệ thống nhắn tin NATS (
nats.zig) và từ đó đào sâu các khả năng mạng của Zig
Sự xuất hiện của thư viện Zio
- Dựa trên những trải nghiệm đó, tác giả công bố Zio: thư viện I/O bất đồng bộ và đồng thời cho Zig
- Zio hướng tới việc viết mã bất đồng bộ không cần callback; bên trong là I/O bất đồng bộ nhưng nhìn từ bên ngoài lại có cấu trúc giống mã đồng bộ
- Đây là một bản hiện thực có giới hạn của mô hình đồng thời kiểu Go sao cho phù hợp với Zig
- Task trong Zio có dạng coroutine kiểu stackful với stack kích thước cố định
- Khi gọi
stream.read(), tác vụ I/O sẽ chạy ở nền, và khi hoàn tất thì task được tiếp tục để trả về kết quả
- Cách làm này đồng thời mang lại quản lý trạng thái đơn giản hơn và cải thiện khả năng đọc mã
Thành phần tính năng và cấu trúc runtime
- Zio hỗ trợ I/O mạng và tệp bất đồng bộ đầy đủ, primitive đồng bộ (mutex, biến điều kiện, v.v.), channel kiểu Go, giám sát tín hiệu hệ điều hành và nhiều thành phần khác
- Task có thể chạy ở chế độ đơn luồng hoặc đa luồng
- Trong chế độ đa luồng, task có thể di chuyển giữa các thread, giúp giảm độ trễ và cải thiện cân bằng tải
- Zio hiện thực giao diện Reader/Writer chuẩn, nhờ đó đảm bảo khả năng tương thích với các thư viện bên ngoài
Hiệu năng và so sánh
- Tác giả cho biết hiện chưa công bố benchmark chính thức, nhưng đã xác nhận hiệu năng ở chế độ đơn luồng nhanh hơn Go và Tokio của Rust
- Chi phí chuyển ngữ cảnh thấp ngang mức gọi hàm, gần như miễn phí trong thực tế
- Chế độ đa luồng hiện chưa vững chắc bằng Go/Tokio, nhưng vẫn cho hiệu năng tương đương hoặc nhỉnh hơn đôi chút
- Trong tương lai, khi bổ sung tính năng fairness, hiệu năng có thể giảm đi phần nào
Mã ví dụ và ứng dụng
- Tài liệu có kèm mã ví dụ máy chủ HTTP dựa trên Zio
- Sử dụng
zio.net.Stream để chấp nhận kết nối và xử lý từng kết nối trong một task riêng
zio.Runtime chịu trách nhiệm quản lý việc chạy task và lập lịch I/O
- Cấu trúc này cho phép viết I/O bất đồng bộ như thể là mã đồng bộ, đồng thời giúp điều khiển luồng rõ ràng và quản lý giải phóng tài nguyên tốt hơn
Kế hoạch tiếp theo và ý nghĩa
- Thông qua Zio, tác giả xác nhận Zig có thể vượt ra ngoài vai trò một ngôn ngữ cho mã hệ thống hiệu năng cao để trở thành ngôn ngữ phát triển ứng dụng mạng hoàn chỉnh
- Bước tiếp theo là viết lại client NATS dựa trên Zio và phát triển thư viện client/server HTTP dựa trên Zio
- Dự án này đang thúc đẩy việc mở rộng hạ tầng mạng và đồng thời của hệ sinh thái Zig, và được đánh giá là một nỗ lực xây dựng mô hình runtime hiện đại có thể sánh với Go hay Rust
1 bình luận
Ý kiến trên Hacker News
Không rõ thiết kế async của Zig có dùng cặp call/return của phần cứng hay được biên dịch sang dạng dựa trên nhảy gián tiếp
Muốn benchmark chuẩn thì phải so sánh tổng thời gian chạy giữa một chương trình liên tục chuyển đổi giữa hai tác vụ và một chương trình hoàn toàn đồng bộ. Việc này khá khó
Nếu kiểm soát được compiler, cũng có thể thay call/ret trong mã I/O bằng các lệnh jump tường minh
Về lâu dài, mong CPU sẽ có meta-predictor để dự đoán tốt hơn cho coroutine có stack
Bài liên quan: Zig new async I/O
Tôi đang dùng Zig trong môi trường embedded (ARM Cortex-M4, 256KB RAM), và dùng nó để có an toàn bộ nhớ khi interoperating với C
Tôi thích async có màu như Rust hơn. Cảm giác “ma thuật” khi nó trông như mã đồng bộ thì rất hay, nhưng trong codebase lớn, vấn đề là khó phân biệt hàm nào có thể blocking
CPU không thực sự bị block bởi I/O, và bản thân OS thread là coroutine có stack do OS hiện thực
Chỉ là ở cấp ngôn ngữ, ta có thể hiện thực ảo giác này hiệu quả hơn, còn bản chất thì giống nhau
Màu được quyết định bởi việc hàm có thực hiện I/O hay không, và tại điểm gọi sẽ ghi rõ có async hay không
Zig cũng đang hướng tới khả năng tính kích thước stack cần thiết khi gọi hàm, nên được kỳ vọng sẽ giảm vấn đề lãng phí RAM của coroutine có stack
Nhưng dự án vẫn rất năng động, và tôi đánh giá cao việc họ ưu tiên thiết kế đúng đắn hơn phát hành nhanh
Hiện tại tôi dùng Go hoặc C và chờ 1.0
Tôi cũng định chờ 0.16 cho các công việc thiên về I/O
Mã cũ vẫn chạy nguyên vẹn, còn API mới thì công thái học hơn và hiệu năng tốt hơn
Tôi cũng đang chuyển dự án cũ sang API Reader/Writer mới và mã nguồn đã gọn gàng hơn hẳn
Cách tiếp cận như libtask trông sạch sẽ hơn nhiều
Rust cũng chọn async kiểu callback, nhưng tôi không rõ vì sao
Tham khảo: libtask
Nhưng nếu trực tiếp đụng vào stack thì có thể xung đột với xử lý ngoại lệ, GC, debugger, v.v.
Ngoài ra cũng khó hợp nhất những thay đổi như vậy ở mức LLVM, nên từ góc độ người thiết kế ngôn ngữ có rất nhiều ràng buộc thực tế
Quá nhỏ thì tràn, quá lớn thì lãng phí bộ nhớ
Kích thước stack cần thiết còn khác nhau giữa các nền tảng nên cũng có vấn đề về tính di động
Nếu Zig giải quyết xong issue #157 thì cách tiếp cận này có lẽ sẽ tốt hơn
Tức là có ba cách triển khai async
Rust biến nó thành state machine tĩnh rồi runtime sẽ polling
Kiểu có stack gây lãng phí bộ nhớ lớn và khó quản lý kích thước stack
Rust chọn cấu trúc không stack để tránh điều đó, còn Zig thì dự kiến sẽ cho phép chọn cả hai cách
Tham khảo: mã coroutine của zio
setsockoptZig cung cấp lớp API POSIX
Tham khảo: tài liệu setsockopt
std.Io.Readercủa Zig chưa nhận biết timeoutĐang hình dung một cấu trúc hoạt động giống
asyncio.timeoutcủa PythonMã ví dụ:
Thực ra đó mới là phần khó nhất
Tham khảo: zio.dev
Nhưng Zig dù là ngôn ngữ cấp thấp vẫn có thể biểu đạt API cấp cao rất gọn gàng, điều đó gây ấn tượng với tôi
Cả Zig lẫn Go đều mới có binding Qt
Tôi muốn có binding cho Rust. cxx-qt là dự án duy nhất còn được duy trì, nhưng tôi không muốn dùng QML hay CMake. Tôi muốn dùng Qt chỉ với Rust + Cargo