Gọi Lại Hay Hàng Đợi Tin Nhắn
Trong quá trình phát triển thư viện socket cho Hive gần đây, tôi đã gặp phải một vấn đề kinh điển mà mình nhớ đã từng đối mặt trước đó. Đây là một trải nghiệm đáng ghi lại để chia sẻ.
Thực tế底层 socket tồn tại một API poll thu thập các sự kiện thông qua epoll/kqueue/select. Làm thế nào để bao bọc (wrap) cơ chế này bằng Lua một cách hiệu quả?
Một giải pháp rõ ràng là tiêm (inject) một hàm gọi lại (callback function) để xử lý từng sự kiện. Tuy nhiên cách tiếp cận này tiềm ẩn nhiều rủi ro: hàm callback có thể ném ra ngoại lệ, gây ra hiện tượng gọi đệ quy (reentrancy) hoặc thực hiện các hành vi không mong muốn. Việc xây dựng cơ chế kiểm soát toàn diện sẽ làm trầm trọng thêm vấn đề hiệu suất vốn tồn tại ở ranh giới C/Lua.
Do đó tôi chọn phương án hai: trả về toàn bộ sự kiện và dữ liệu liên quan để giao cho mã Lua xử lý ở tầng cao hơn. Tuy nhiên phương pháp này cũng gây áp lực lên bộ thu gom rác (GC) của Lua VM khi phải liên tục tạo dựng các cấu trúc dữ liệu phức tạp tạm thời.
Để tối ưu hóa, tôi thiết kế việc truyền vào một bảng trống từ phía Lua như một hàng đợi tin nhắn chung gian. Ở tầng C sử dụng hàm rawseti - một API Lua cực kỳ hiệu quả khi thao tác với bảng có khóa là số nguyên liên tiếp, hiệu suất ngang với mảng trong C.
Với mỗi tin nhắn chứa nhiều field dữ liệu, tôi xây dựng cấu trúc hai chiều dạng bảng lồng bảng. Để tránh việc tạo ra lượng lớn bảng tạm thời, tôi xây dựng cơ chế cache tái sử dụng bảng đã qua sử dụng. Ví dụ khi nhận 4 tin nhắn đầu tiên, sẽ tạo 4 bảng tương ứng. Khi nhận tiếp 5 tin nhắn mới, chỉ tạo thêm 1 bảng mới và tái sử dụng 4 bảng cũ. Khi hệ thống ổn định, toàn bộ cấu trúc hai chiều này sẽ được xây dựng sẵn, loại bỏ hoàn toàn việc tạo bảng tạm thời.
Nhân đây xin chia sẻ một giải pháp hàng đợi tin nhắn đơn giản trong Lua mà tôi tâm đắc. Chỉ với vài dòng mã ngắn gọn nhưng cực kỳ hiệu quả:
local hang_doi_moi = {} local hang_doi_cu = {}
– Thêm tin nhắn mới table.insert(hang_doi_moi, tin_nhan_moi)
– Xử lý tin nhắn theo cơ chế chuyển đổi 2 hàng đợi while hang_doi_moi[1] do hang_doi_cu, hang_doi_moi = hang_doi_moi, hang_doi_cu for i=1,#hang_doi_cu do xu_ly(hang_doi_cu[i]) – Xử lý tin nhắn hang_doi_cu[i] = nil – Giải phóng bộ nhớ end end
Cơ chế này đảm bảo xử lý triệt để cả các tin nhắn phát sinh trong quá trình dispatch, đồng thời tối ưu hiệu suất thông qua việc tái sử dụng cấu trúc dữ liệu.