2 điểm bởi GN⁺ 2025-10-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ơncả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

 
GN⁺ 2025-10-28
Ý kiến trên Hacker News
  • Người ta nói việc chuyển ngữ cảnh ở mức gọi hàm gần như miễn phí, nhưng trên thực tế vẫn có những chi phí tinh vi như làm hỏng bộ dự đoán nhánh (branch predictor)
    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ó
    • Với coroutine không stack, nếu liên tục chuyển giữa hai tác vụ ở đáy call stack và mã chuyển stack được inline thì có thể tránh phần lớn mức phạt do call/ret không khớp
      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
    • Hiện tại Zig đã bỏ async ở cấp ngôn ngữ, và OP tự triển khai chuyển tác vụ trong không gian người dùng
    • Khi thử bài test ping-pong đơn giản giữa các coroutine, đã từng thu được các con số khó tin khi so với những giải pháp khác
    • Zig sắp có async mới, nên đang chờ trước khi đào sâu nghiêm túc
      Bài liên quan: Zig new async I/O
  • Coroutine có stack có ý nghĩa khi RAM đủ nhiều
    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
    • Thực ra toàn bộ mã đồng bộ đều là một ảo giác do phần mềm tạo ra
      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
    • IO mới của Zig sẽ có cấu trúc có màu (colored) còn tinh tế hơn cả Rust
      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
    • Đó cũng chính là lý do Zig muốn biểu diễn I/O một cách tường minh, để có thể theo dõi hàm nào blocking
  • Có ý kiến cho rằng hiện giờ còn quá sớm để dùng Zig. Mô hình I/O đang thay đổi lớn nên có cảm giác sẽ mất vài năm nữa
    • Tôi cũng đã rời Zig vào năm 2020 vì lý do tương tự.
      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
    • Vài năm trôi qua rất nhanh. Zig đã là một ngôn ngữ đủ dùng rồi. Ai muốn dùng thì dùng, ai không muốn thì thôi
    • Thực ra đúng là thời điểm không tốt. 0.16 dự kiến có thay đổi I/O lớn, và ngay cả tác giả bài viết cũng chưa dùng tính năng mới nhất
      Tôi cũng định chờ 0.16 cho các công việc thiên về I/O
    • Nhưng nếu là công việc liên quan đến I/O thì dùng giao diện buffered reader/writer của Zig 0.15 cũng sẽ không có thay đổi quá lớn
    • Tôi lại nghĩ ngược lại, hiện giờ không phải lúc tệ. Zig không phải đang thay đổi chóng mặt ở bản thân ngôn ngữ, mà đang bổ sung API std.Io mới mạnh mẽ
      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
  • Tôi vẫn thắc mắc vì sao async kiểu callback lại trở thành tiêu chuẩ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
    • Coroutine không stack có thể được hiện thực ngay trong ngôn ngữ, và ưu điểm là tương tác có thể dự đoán được với các tính năng sẵn có
      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ế
    • Kết quả nghiên cứu của Microsoft cho chuẩn C++ cho thấy coroutine không stack có overhead bộ nhớ thấp hơn rất nhiều, đồng thời cho phép tự do hơn trong thiết kế executor
    • Nhược điểm của cách làm như zio hay libtask là phải tự ước lượng kích thước stack
      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
    • Với libtask, kích thước stack của thread khá mơ hồ và lớn hơn nhiều so với trạng thái async thông thường
    • Async của Rust không phải callback mà là polling
      Tức là có ba cách triển khai async
      1. Dựa trên callback (Node.js, Swift)
      2. Dựa trên stack đầy đủ (Go, libtask)
      3. Dựa trên polling (Rust)
        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
  • Một lệnh đọc TCP có thể block cả một tháng, nên tôi tò mò giao diện timeout cho I/O sẽ như thế nào
    • Trên socket TCP có thể đặt timeout đọc/ghi bằng setsockopt
      Zig cung cấp lớp API POSIX
      Tham khảo: tài liệu setsockopt
    • Hiện tại std.Io.Reader củ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.timeout của Python
      Mã ví dụ:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • Hầu hết framework async đều bỏ qua timeout và hủy tác vụ
      Thực ra đó mới là phần khó nhất
  • Scala đã có thư viện đồng thời ZIO rồi
    Tham khảo: zio.dev
  • Gần đây tôi rất ấn tượng với Tokio của Rust, và nếu Zig cũng có thể hiện thực đồng thời kiểu Go mà không cần GC thì tôi rất muốn thử
    • Go có thể dùng các thủ thuật như stack mở rộng vô hạn nhờ có GC
      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
  • Tôi biết đến Zig lần đầu qua website của Bun. Dạo này nó thực sự phát triển rất nhanh
  • Ở phiên bản C++ trước đây, tôi đã hiện thực I/O bất đồng bộ bằng Qt, còn lần này thì chuyển sang Go
    Cả Zig lẫn Go đều mới có binding Qt
    • Go: miqt
    • Zig: libqt6zig
      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
  • Trong Scala cũng đã có framework nổi tiếng tên là ZIO, nên đúng là chuyện đặt tên rất khó