Thư Viện Đa Nhiệm Ltask Cho Lua - nói dối e blog

Thư Viện Đa Nhiệm Ltask Cho Lua

Ngày 7 tháng 2 năm 2021: Thư viện này đã được thay thế bằng một phiên bản hoàn toàn mới. Chi tiết xem tại:

Câu chuyện bắt đầu khi nhóm kỹ thuật của chúng tôi phỏng vấn một ứng viên. Dự án gần đây nhất của bạn ấy là một hệ thống tên là luajit.io. Sau buổi phỏng vấn, bạn ấy đã dành vài giờ đồng hồ để thảo luận sâu về dự án này với tôi. Ý tưởng cốt lõi là xây dựng một web server dựa trên LuaJIT, tương tự như ngx_lua nhưng không phụ thuộc vào Nginx. Bạn ấy chia sẻ nhiều vấn đề gặp phải với LuaJIT, nhưng vì lý do hiệu năng nên không muốn chuyển sang Lua thuần.

Tôi gợi ý rằng thay vì xây dựng một ứng dụng host độc lập sử dụng LuaJIT như ngôn ngữ nhúng, tại sao không thử phát triển thành một thư viện dành riêng cho Lua? Cách tiếp cận này sẽ mở ra nhiều khả năng hơn, không bị giới hạn bởi việc người dùng chọn Lua hay LuaJIT. Dù thiết kế thư viện phức tạp hơn nhiều so với viết ứng dụng host, tôi vẫn quyết định thử nghiệm.

Lua có khá nhiều thư viện đa nhiệm. Bạn có thể tham khảo chi tiết trên Lua User Wiki. Các giải pháp phổ biến bao gồm:

  • Dùng coroutine của Lua để mô phỏng đa nhiệm mà không cần thread hệ điều hành. Chỉ cần viết một bộ lập lịch bằng Lua. Với tính năng cho phép yield từ debug hook (dù hiện tại cần C để triển khai hook), thậm chí có thể tạo bộ lập lịch dạng preemptive.
  • Mỗi OS thread chạy một Lua VM độc lập, các VM giao tiếp qua cơ chế message passing.

Hầu hết các thư viện này tập trung giải quyết nhu cầu như web server, nên thường tích hợp luôn giao diện mạng để đồng bộ hóa IO. Tuy nhiên, tôi nhận thấy cần một thư viện thuần túy hơn, chỉ tập trung vào lập lịch đa nhiệm.

Tôi chọn mô hình M:N thay vì N:N (1 OS thread = 1 Lua VM) vì:

  1. Tạo/làm sạch Lua VM rất nhẹ, tiêu tốn ít bộ nhớ
  2. Cách ly giữa các VM giúp dễ quản lý hơn
  3. Mô hình M:N tận dụng tối đa khả năng song song

Về mặt thiết kế, tôi tách biệt rõ ràng:

  • Cơ sở hạ tầng: Channel giao tiếp (bắt buộc)
  • Cơ sở tầng cao: Network IO, Timer (tùy chọn)

Ví dụ, bạn có thể dùng thư viện levent (dựa trên libev) trong một task, sau đó truyền dữ liệu qua channel cho các task xử lý khác.

Chỉ trong 2 ngày, tôi hoàn thiện giao diện và triển khai cơ bản. Mã nguồn hiện có trên GitHub: . Phiên bản hiện tại cung cấp:

  • Bộ lập lịch M:N
  • Hệ thống channel tích hợp

Cách sử dụng:

  1. Khởi tạo scheduler trong chương trình chính, cấu hình số worker thread
  2. Scheduler sẽ chạy mãi mãi (ở phiên bản thử nghiệm)
  3. Các task (file Lua) cần được khởi động trước khi chạy scheduler
  4. Mỗi task là một Lua VM độc lập, có thể truyền tham số tùy ý

Cơ chế hoạt động:

  • Scheduler phân phối task đều cho các worker thread
  • Khi một worker rảnh, nó sẽ “đánh cắp” task từ hàng đợi của worker khác
  • Channel được định danh bằng ID số nguyên, đảm bảo an toàn khi truyền giữa các task
  • Mỗi channel hỗ trợ đọc/ghi không chặn, có thể có nhiều reader/writer đồng thời

Điểm đặc biệt của channel:

  • Không chặn khi đọc rỗng, trả về ngay
  • Cần dùng select để kiểm tra nhiều channel cùng lúc
  • Nếu tất cả channel đều rỗng, task sẽ yield và bị đánh dấu blocked cho đến khi có dữ liệu mới

So với Skynet, ltask có một số khác biệt:

Tiêu chí Skynet ltask
Mô hình Message-driven Event-driven
Channel Mỗi service 1 channel Task có thể theo dõi nhiều channel
Phân chia thời gian Theo từng message Tự do yield khi cần
Tính linh hoạt Hiệu năng cao hơn chút Tổng quát hơn

Triển khai thread pool:
Tôi không dùng thư viện bên thứ ba mà tự viết một lớp bọc nhẹ (~100 dòng) cho pthread và Windows API. Bạn có thể

Lợi thế của ID channel:
Thay vì dùng con trỏ đối tượng (dễ gây lỗi trong môi trường đa luồng), ltask dùng ID số nguyên để định danh channel. Đây là giải pháp ổn định và an toàn hơn nhiều.

Việc hoàn thiện ltask trong 1.5 ngày là kết quả của nhiều công việc chuẩn bị trước đó. Hy vọng thư viện này sẽ mở ra những hướng tiếp cận mới cho các hệ thống đa nhiệm dựa trên Lua.

0%