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

Ltask: Thư Viện Đa Nhiệm Của Lua

ltask - Thư viện đa nhiệm cho Lua
ltask là thư viện đa nhiệm cho ngôn ngữ Lua mà tôi đã phát triển cách đây hai tuần. Dự án này kế thừa tên gọi từ một dự án tương tự trước đây của tôi, tuy nhiên thiết kế hoàn toàn mới đã được xây dựng từ đầu với mục tiêu tương tự nhưng tiếp cận hiện đại hơn. Tôi đã quyết định xóa bỏ kho mã nguồn cũ và tạo một kho mã mới với cùng tên gọi này.

Khác biệt lớn so với phiên bản trước nằm ở cơ chế truyền thông điệp. Phiên bản hiện tại gần giống mô hình của Skynet nhưng được triển khai dưới dạng thư viện thay vì framework. Module lập lịch (scheduler) được xây dựng dựa trên ý tưởng đã đề xuất trước đó, tuy nhiên hiện mới chỉ ở dạng nguyên mẫu sơ khai và cần nhiều cải tiến chi tiết hơn.

Thư viện phần C cung cấp 4 thành phần chính:

  1. ltask - lõi hệ thống
  2. ltask.bootstrap - công cụ khởi tạo hệ thống
  3. ltask.exclusive - module cho dịch vụ độc quyền
  4. ltask.root - module quản lý dịch vụ gốc

Khi sử dụng trình thông dịch Lua chính thống làm điểm khởi đầu, mã nguồn đầu vào chỉ nên sử dụng duy nhất module ltask.bootstrap. Tập API của module này cho phép xây dựng toàn bộ kiến trúc hệ thống, tương tự như phần khởi tạo và quản lý luồng xử lý trong Skynet nhưng được đóng gói dưới dạng thư viện tiện dụng. Sau khi hoàn tất cấu hình, API ltask.bootstrap.run sẽ kích hoạt bộ lập lịch và khóa luồng chính, mọi xử lý tiếp theo sẽ do các luồng công việc đã được thiết lập điều phối.

Tương tự Skynet, mọi tác vụ (task) đều được thực hiện trong các “dịch vụ” (service). Mỗi dịch vụ là một máy ảo Lua độc lập với kênh truyền thông riêng, được định danh bằng 32-bit. Khác với Skynet, phần nhân hệ thống không quản lý tên dịch vụ. Dịch vụ ID 0 là ID dự phòng cho các dịch vụ hệ thống hoặc không hợp lệ, trong khi dịch vụ ID 1 là dịch vụ gốc (root) có đặc quyền đặc biệt. Tôi dự kiến dành riêng dải ID từ 2 đến 1023 cho các dịch vụ hệ thống quan trọng như phân giải tên, quản lý timer, socket… để tránh dùng tên chuỗi.

Module ltask.bootstrap cung cấp các API để cấu hình hệ thống: khởi tạo dịch vụ nhưng chưa chạy ngay. Khác biệt quan trọng là việc phân loại dịch vụ thành hai dạng:

  • Shared (Chia sẻ): Các dịch vụ chia sẻ N luồng xử lý, tương tự Skynet
  • Exclusive (Độc quyền): Dịch vụ chạy trên luồng hệ thống riêng, đặc biệt phù hợp cho các tác vụ chặn (blocking) như socket select/epoll

Dịch vụ exclusive cho phép tích hợp các thư viện bên thứ ba (MQ, DB driver…) mà không lo chiếm dụng luồng xử lý. Hiện tại tôi mới chỉ triển khai dịch vụ timer để gửi thông điệp định kỳ. Mặc dù dịch vụ exclusive có thể nhận thông điệp nội bộ, nhưng để tối ưu hiệu suất, timer được quản lý bằng spinlock thay vì thông điệp.

Dịch vụ gốc có đặc quyền tạo/xóa dịch vụ. Để tránh dùng khóa (lock) phân mảnh, việc tạo/xóa handle dịch vụ chỉ được thực hiện khi có quyền điều phối. Các API liên quan được tích hợp trực tiếp vào dịch vụ gốc để đảm bảo an toàn. Dịch vụ exclusive cũng có đặc quyền gửi hàng loạt thông điệp (batch) thông qua module ltask.exclusive, trong khi dịch vụ shared chỉ được gửi từng thông điệp đơn lẻ.

Mô hình thông điệp của ltask có điểm khác biệt then chốt: Mỗi thông điệp gửi đi sẽ làm treo dịch vụ cho đến khi nhận được biên nhận (receipt). Điều này đảm bảo logic nghiệp vụ không bị ngắt quãng bất kể là send hay call. Dưới lớp cơ sở, mọi thông điệp đều có session và phản hồi, nhưng API send sẽ bỏ qua phản hồi trong khi call sẽ treo coroutine hiện tại chờ phản hồi.

Biên nhận có ba trạng thái:

  1. Thành công
  2. Dịch vụ đích không tồn tại
  3. Hàng đợi thông điệp của đích đang bận

Hiện tại, khi hàng đợi bận sẽ ném lỗi để tránh hiệu ứng tuyết lở khi hệ thống quá tải. Trong tương lai có thể bổ sung cơ chế thử lại hoặc ghi log.

Hầu hết logic nghiệp vụ được triển khai ở lớp Lua, phần C chỉ cung cấp cơ chế nền tảng. Dịch vụ shared nên xử lý xong tác vụ rồi treo bằng coroutine.yield, chờ được đánh thức lại bởi bộ lập lịch. Các API tối giản cho phép đọc thông điệp, gửi thông điệp và nhận biên nhận.

Dịch vụ exclusive được thiết kế theo mô hình message-based với vòng lặp xử lý thông điệp kết hợp các lệnh hệ thống (sleep, select…), sau mỗi tác vụ sẽ yield CPU cho bộ lập lịch. Nhờ có luồng hệ thống riêng, không lo bị đói tài nguyên.

Với dịch vụ shared, tôi đã xây dựng lớp bọc (wrapper) đơn giản hóa việc phát triển, tương tự Skynet nhưng tối ưu hơn. Mỗi dịch vụ đăng ký bảng xử lý, với mỗi yêu cầu sẽ tạo coroutine riêng và tự động treo/phục hồi khi cần.

Về bộ lập lịch, có ba chức năng chính:

  1. Xử lý thông điệp từ mọi luồng, phân phát và tạo biên nhận
  2. Thu gom tác vụ hoàn thành từ các luồng công việc
  3. Phân bổ dịch vụ từ hàng đợi sang các luồng rảnh

Bộ lập lịch không phải luồng riêng biệt mà là module có thể được gọi từ mọi luồng nhưng chỉ một luồng giữ quyền điều khiển tại một thời điểm. Khi luồng công việc hoàn thành tác vụ mà không có việc mới, nó sẽ cạnh tranh quyền điều khiển bộ lập lịch.

Hiện tại, tôi đang triển khai ltask cho engine phía client với mục tiêu đa nền tảng (kể cả Windows). Do yêu cầu mạng không cao, tôi sẽ dùng các thư viện sẵn có như luasocket thay vì移植 Skynet’s network module. Về tối ưu hóa, tôi dự kiến thay thế cơ chế serialize thành cấu trúc dữ liệu tùy chỉnh để trao đổi giữa các dịch vụ, giúp tăng hiệu suất.

Các công việc tiếp theo cần hoàn thiện:

  • Tối ưu phân bổ dịch vụ theo luồng xử lý trước đó
  • Bổ sung cơ chế xử lý hàng đợi bận linh hoạt
  • Triển khai thêm dịch vụ hệ thống thiết yếu
  • Tích hợp mạng cho client engine
0%