Triển Khai Đa Luồng Chiếm Quyền Trong Lua
Phiên bản Lua 5.2 đã bắt đầu được phát triển từ tháng 1 năm 2010. Quá trình phát triển kéo dài gần hai năm trời, đến nay终于 ra mắt phiên bản beta. Mình rất mong chờ phiên bản chính thức sẽ được phát hành trong năm 2011. Trong hai năm đầy biến động đó, nhiều tính năng mới đã được đề xuất, nhưng cuối cùng bị loại bỏ khi cân nhắc kỹ lưỡng.
Khi nhìn vào danh sách cải tiến, dường như không có gì quá đột phá. Tuy nhiên, nếu đọc kỹ mã nguồn, bạn sẽ thấy phần lớn hệ thống bên trong đã được viết lại để tương thích với những thay đổi bề ngoài tưởng chừng nhỏ nhặt. Với người hiểu rõ Lua, cải tiến đáng chú ý nhất là khả năng “yield được trong pcall và metamethod”, được liệt kê đầu tiên trong mục thay đổi chính của Lua. Trong khi đó, tính năng goto mới lại được xếp ở vị trí cuối cùng.
Tất nhiên, đây chỉ là nhận định ban đầu của mình. Chưa được dùng thử 5.2 trong thời gian dài, việc đánh giá lúc này có thể còn vội vàng. Nhưng mình muốn chia sẻ trước về tiềm năng của cải tiến này đối với công việc phát triển phần mềm.
Tính năng yield của coroutine giờ đây gần như có thể được sử dụng ở mọi nơi. Mình nhấn mạnh chữ “gần như” vì vẫn tồn tại một vài giới hạn nhất định. Việc giải thích cụ thể về các giới hạn này khá phức tạp, và mình đã mất nguyên một ngày để nghiên cứu mã nguồn beta 5.2. Sẽ có dịp bàn kỹ hơn trong bài viết khác, còn hôm nay chỉ tập trung vào ứng dụng thực tế.
Chúng ta biết rằng coroutine cho phép xây dựng mô hình đa luồng hợp tác - mỗi “luồng” (coroutine) chỉ dừng lại khi người dùng mong muốn, đồng thời có thể tiếp tục từ nơi đã dừng. Điều này giải quyết nhiều vấn đề rườm rà của đa luồng chiếm quyền truyền thống. Trong một cuộc phỏng vấn, cha đẻ của Lua từng nhấn mạnh lợi ích của coroutine trong xử lý song song. Tuy nhiên, việc phải viết yield thường xuyên gây khó chịu. Các framework thường ẩn đi các lệnh yield này - như cách tiếp cận ban đầu của framework Kepler từng giấu yield trong quá trình xuất HTML. Một giải pháp tinh tế hơn là tạo debug hook trong Lua, gọi yield từ bên trong hook đó. Điều này cho phép máy ảo Lua tự động yield sau mỗi vài dòng byte code.
Tiếc rằng điều này không khả thi trong Lua 5.1 trở về trước do các giới hạn nghiêm ngặt về yield. Nếu yield xảy ra bên trong metamethod hoặc pcall, hệ thống sẽ thất bại. Điều này liên quan đến cơ chế chuyển đổi giữa hàm C và hàm Lua khi thực thi pcall hay metamethod. Khi yield xảy ra ở tầng sâu của các hàm lồng nhau, cơ chế longjmp sẽ làm mất trạng thái stack frame của hàm C. Dù trạng thái máy ảo Lua (L) vẫn được bảo lưu, nhưng trạng thái C bị mất khiến việc quay lại gặp lỗi. Lua 5.2 giải quyết vấn đề này bằng API mới, có thể xem chi tiết tại mục 4.7 “Handling Yields in C” trong tài liệu chính thức.
Việc sử dụng debug library mặc định không đủ. Hàm hook thiết lập qua debug.sethook không thể gọi coroutine.yield do giới hạn trong cách triển khai Lua. Giải pháp đúng là dùng trực tiếp C API lua_sethook
với hàm hook như sau:
|
|
Bạn có thể tùy chọn để hook kích hoạt sau mỗi vài dòng code hoặc mỗi lần gọi hàm. Một khi cài đặt thành công, đoạn mã Lua sẽ tự động yield định kỳ mà không cần can thiệp thủ công.
Lợi ích cụ thể thế nào? Mình hình dung hai ứng dụng chính:
- Thư viện đa luồng chiếm quyền dựa trên Lua: Về bản chất vẫn dùng coroutine, nhưng việc chuyển đổi luồng được thực hiện tự động qua debug hook định kỳ. Điều này giúp cải tiến các công cụ đã có, mang lại trải nghiệm người dùng tốt hơn.
- Quản lý nhiều Lua state độc lập: Khởi động nhiều state trong một tiến trình hệ điều hành, mỗi state chỉ dùng một main thread. Thiết lập debug hook để main thread tự động yield sau từng giai đoạn, trả lại quyền điều khiển cho mã C cấp trên. Tại tầng C, xây dựng bộ lập lịch hiệu quả. Các Lua state hoàn toàn độc lập, giao tiếp qua thư viện như ZeroMQ, thậm chí phân bổ lên nhiều OS thread. Mô hình M:N này hiệu quả hơn nhiều so với đa luồng hệ điều hành truyền thống, đồng thời tiết kiệm tài nguyên stack. Đây là cách tiếp cận tương đồng với mô hình của Erlang.
Thực tế, các phiên bản Lua trước đây đã có thể thực hiện yield không giới hạn qua thư viện lua coco. Tuy nhiên, coco phụ thuộc vào thư viện fiber của hệ điều hành, gây tốn kém thêm stack memory so với giải pháp thuần Lua 5.2.
Về các ý tưởng đang ấp ủ: Liệu engine AI của NPC trong game có thể chạy độc lập trên các Lua state riêng biệt, giao tiếp qua tin nhắn không? Việc tạo sẵn pool state để gán động các AI mới sẽ giảm tải chi phí khởi tạo. Chia nhỏ nhiệm vụ thành các đơn vị độc lập chạy trên state riêng không chỉ giúp quản lý dễ dàng mà còn tối ưu hóa garbage collection của Lua.
(P/s: Mình từng tham gia thảo luận về hiệu năng giữa mô hình M:N ở cấp ngôn ngữ và cấp hệ điều hành trên Google+ ngày 12/7 vừa qua. Tuy nhiên cuộc trò chuyện diễn ra trong nhóm kín nên chưa thể chia sẻ rộng rãi.)