Kỹ Thuật Chuyển Đổi Động Giữa Các Luồng Hệ Điều Hành Trong Thời Gian Chạy Lua
Trong quá trình phát triển engine gần đây, chúng tôi đã gặp phải một vấn đề kỹ thuật liên quan đến hệ điều hành và tìm ra một giải pháp hết sức sáng tạo. Tôi cho rằng đây là một chủ đề thú vị đáng được ghi lại để chia sẻ với cộng đồng kỹ thuật.
Trên nền tảng iOS, nếu ứng dụng không kịp xử lý các thông điệp hệ thống (như sự kiện chạm màn hình), hệ điều hành có thể đánh giá ứng dụng đang gặp sự cố và chủ động chấm dứt tiến trình. Điều này đòi hỏi phải có cơ chế xử lý thông điệp hệ thống hiệu quả.
Đối với ứng dụng tự xây dựng hoàn toàn, việc xử lý vòng lặp thông điệp có thể được đặt ở mức ưu tiên cao nhất. Dù có nhiều tác vụ nặng, chỉ cần sắp xếp mã nguồn hợp lý là có thể tránh làm chậm việc xử lý thông điệp. Tuy nhiên khi trở thành một engine, chúng ta khó có thể ngăn người dùng thực hiện các thao tác tốn thời gian làm tắc nghẽn luồng chính. Giải pháp thông thường là tách biệt vòng lặp thông điệp cửa sổ và logic nghiệp vụ vào hai luồng riêng biệt, giúp luồng xử lý thông điệp luôn phản hồi kịp thời.
Trên Windows, hệ thống cho phép chạy vòng lặp thông điệp cửa sổ ở bất kỳ luồng nào. Nhưng với iOS (và Mac), vòng lặp thông điệp bắt buộc phải chạy trên luồng chính (main thread). Điều này tạo ra thách thức đặc biệt khi toàn bộ mã C/C++ của engine đều xuất hiện dưới dạng thư viện mở rộng cho Lua, với kiến trúc cốt lõi được điều phối bởi Lua. Người dùng có thể khởi động engine thông qua bất kỳ trình thông dịch Lua nào, bao gồm cả thư viện đa luồng độc lập mà chúng tôi phát triển. Mỗi luồng sở hữu một máy ảo Lua độc lập, giao tiếp giữa các luồng thông qua channel.
Mâu thuẫn phát sinh ở đây:
- Vòng lặp thông điệp cửa sổ cần được che giấu hoàn toàn khỏi người dùng
- Người dùng mong muốn logic nghiệp vụ chạy trên VM Lua được tạo ra khi khởi động
- Module quản lý cửa sổ yêu cầu chạy trên luồng riêng biệt
- Quản lý cửa sổ bắt buộc phải chạy trên main thread hệ điều hành
- Mỗi luồng hệ điều hành lại sở hữu một VM Lua độc lập
Ban đầu tôi cho rằng mâu thuẫn này không thể giải quyết, trừ khi tự thiết kế trình khởi động Lua đặc biệt. Trong khi đó, các trình thông dịch Lua chuẩn đều có giới hạn rõ rệt. Khi gọi hàm pmain để thực thi Lua code, chỉ cần đoạn code này chưa hoàn tất, quá trình sẽ tiếp tục chạy. Điều này khiến stack gọi luôn tồn tại ít nhất một hàm Lua chưa trả về, khiến việc chuyển đổi giữa các luồng hệ điều hành trở nên bất khả thi.
Tuy nhiên, sáng kiến mới đây đã mở ra hướng giải quyết đột phá. Chúng tôi đã bổ sung API mới cho thư viện luồng:
|
|
Ý nghĩa của API này:
- Tạo ra hai luồng xử lý song song
func1
: Closure chạy trên VM hiện tại, có toàn quyền truy cập trạng thái VMfunc2
: Luồng xử lý độc lập, được cung cấp dưới dạng mã nguồn, sẽ được load vào VM mới trên luồng hệ điều hành khác. Không thể truy cập VM gốc, bắt buộc dùng channel để giao tiếp- Hàm fork chỉ trả về khi cả func1 và func2 hoàn tất (hoặc ném ngoại lệ)
- Giá trị trả về là kết quả từ func1, kết quả func2 sẽ bị bỏ qua
Điểm đột phá nằm ở việc chúng tôi có thể linh hoạt chọn luồng nào chạy func1/func2. Thay vì bắt buộc func1 phải chạy trên luồng hiện tại, chúng tôi cho phép:
- Chuyển VM hiện tại sang luồng mới cho func1
- Tạo VM mới trên luồng hiện tại cho func2
Nhờ cơ chế pcall bao bọc cả func1 và func2, chúng sẽ không thể thoát khỏi phạm vi kiểm soát của fork. Khi truyền trạng thái L (Lua state) sang luồng mới cho func1, luồng hiện tại tiếp tục chạy func2 với VM mới mà không gây xung đột.
Giải pháp này mang lại lợi thế kép:
- Người dùng không cần quan tâm đến việc chuyển đổi luồng hệ điều hành
- Đảm bảo vòng lặp thông điệp cửa sổ luôn chạy trên main thread theo yêu cầu của iOS
- Bảo tồn toàn vẹn ngữ cảnh VM gốc cho logic nghiệp vụ
Đây thực sự là một giải pháp tinh tế, kết hợp thành công yêu cầu kỹ thuật phức tạp với trải nghiệm phát triển trực quan cho người dùng.