Hành Trình Gian Nan Của Lua 5.4.4 - nói dối e blog

Hành Trình Gian Nan Của Lua 5.4.4

Ngày 21 tháng 12 năm 2021, phiên bản Lua 5.4.4 rc2 chính thức được công bố. Đây là một phiên bản mang tính “thai nghén” đầy khó khăn. Các phiên bản nhỏ trước đây từ rc1 đến khi phát hành chính thức thường chỉ mất tối đa hơn mười ngày, nhưng lần này đã vượt quá một tháng mà vẫn chưa thể hoàn tất.

Ngay sau vài giờ phát hành rc2, qua cuộc thảo luận trên danh sách email, một lỗi nghiêm trọng đã được phát hiện. Điều thú vị là lỗi này không phải do phiên bản 5.4.4 gây ra, mà đã tồn tại âm ỉ hơn một thập kỷ. Trước đó khi phát hành rc1 cũng đã tìm thấy lỗi tương tự - không phải từ phiên bản mới. Có vẻ như trong hai năm gần đây, số lượng người tham gia kiểm thử Lua tăng đột biến, cùng với những phương pháp kiểm thử hiện đại đã vén bức màn che giấu bấy lâu, phơi bày hàng loạt lỗi “cổ thụ” mà trước đây chưa từng được phát hiện. Đa phần những lỗi này đều liên quan mật thiết đến hệ thống thu gom rác (GC).

Lỗi liên quan đến GC đặc biệt nan giải vì chúng không dễ dàng bị phát hiện qua kiểm thử đơn vị. Ngay cả khi đã xác định được vị trí, việc phân tích nguyên nhân gốc rễ cũng vô cùng phức tạp, và để sửa triệt để lại càng khó khăn hơn. Khi Lua 5.4 vừa ra mắt, một lỗi trong cơ chế GC thế hệ mới đã gây tranh cãi dữ dội suốt nhiều tuần trước khi được giải quyết dứt điểm. Hai ngày sau khi công bố lỗi, chính tác giả Lua - Roberto Ierusalimschy đã thừa nhận: “Cho đến nay tôi vẫn chưa tìm được manh mối nào. Đây rõ ràng là vấn đề thực sự trong GC, nhưng rất khó tái hiện. Áp dụng các bản sửa trước đây khiến nó biến mất, nhưng tôi không thể hiểu tại sao những bản sửa đó lại có hiệu quả.”

Tôi đã dành nhiều thời gian mày mò phân tích từng chi tiết nhỏ của loại lỗi phức tạp này. Quá trình gỡ từng lớp vấn đề như rút tơ kén mang lại cảm giác thỏa mãn đặc biệt, đồng thời thấm thía nỗi gian nan trong việc đảm bảo hệ thống hoàn hảo không lỗi.

Trong lần này, vấn đề gặp phải với Lua 5.4.4 tuy không nghiêm trọng bằng nhưng vẫn liên quan đến GC, cụ thể là hai lỗi liên quan đến finalizer. Thứ nhất, việc gọi các API thu gom rác trong phương thức gc (finalizer) có thể dẫn đến hiện tượng “re-entrancy” (truy cập lồng nhau), trong khi GC của Lua được thiết kế không cho phép điều này. Tôi hoàn toàn ủng hộ quyết định thiết kế này - bản thân GC đã đủ phức tạp, việc cho phép truy cập lồng nhau sẽ khiến hệ thống dễ phát sinh lỗi hơn nhiều.

Giải pháp cuối cùng là cấm tuyệt đối việc gọi hàm collectgarbage trong finalizer. Thực tế, nhiều lập trình viên chưa nhận thức rõ về giới hạn này: bộ nhớ được sử dụng trong finalizer sẽ không bị thu gom cho đến khi thoát khỏi phương thức đó. Cũng không thể kích hoạt finalizer khác trong quá trình thực thi. Từ góc độ triển khai, giới hạn này hoàn toàn hợp lý, nhưng với người dùng lại khó hiểu. Khi công bố giải pháp, nhiều người lo ngại liệu việc cấm GC có làm tăng nguy cơ lỗi “out of memory” (OOM). Tuy nhiên, thực tế từ khi Lua có finalizer đến nay, cơ chế này đã tồn tại như vậy.

Vấn đề thứ hai liên quan đến thứ tự thực thi finalizer. Khi tạo đối tượng mới trong finalizer và thêm finalizer cho chúng, các finalizer mới này sẽ được xếp sau những finalizer đã sẵn sàng nhưng chưa chạy. Điều này dẫn đến rủi ro trong quá trình tắt máy ảo Lua: nếu finalizer mới gọi hàm từ thư viện C động, có thể xảy ra lỗi do thư viện C đã bị gỡ bỏ trước đó. Nguyên nhân nằm ở việc thư viện C thường được gỡ bởi finalizer đăng ký đầu tiên, theo nguyên tắc “vào trước - ra sau”. Nhưng trong giai đoạn tắt máy, đối tượng mới được tạo sau khi các đối tượng cũ đã giải phóng, dẫn đến việc tham chiếu đến mã đã bị gỡ bỏ. Đặc biệt, hàm C nhẹ (light C function) về bản chất chỉ là con trỏ, không thể đánh dấu (mark) đối tượng thư viện động.

Giải pháp hiện tại là bỏ qua các finalizer mới tạo trong giai đoạn cuối cùng. Tôi cho rằng có thể tối ưu hơn bằng cách đưa các finalizer này vào đầu danh sách để đảm bảo finalizer gốc (dùng để gỡ thư viện C) luôn chạy cuối cùng, thay vì chờ đến chu kỳ đánh dấu-sweep mới.

Tính đến thời điểm hiện tại, Lua 5.4.4 vẫn chưa chính thức phát hành, và tương lai có thể còn thay đổi. Sự việc này minh chứng cho vài nguyên lý quan trọng:

Trước hết, khi sử dụng finalizer, chúng ta nên tuân thủ nguyên tắc “tối giản” - chỉ thực hiện các tác vụ đơn giản, đặc biệt là giải phóng tài nguyên quản lý bởi mã C. Nguyên tắc này không chỉ áp dụng cho Lua mà còn cho mọi ngôn ngữ có hệ thống GC. Quá trình khởi tạo và hủy đối tượng thường là nơi dễ phát sinh lỗi nhất, và sự đơn giản chính là vũ khí duy nhất để đối phó với độ phức tạp này.

Rộng hơn nữa, việc duy trì tính ổn định của phần mềm luôn là thách thức lớn. Đối với hạ tầng cốt lõi, chúng ta cần giữ thiết kế đơn giản và khuyến khích cộng đồng tham gia kiểm thử. Như định luật Linus nổi tiếng trong tác phẩm “Nhà thờ và Chợ” của Eric S. Raymond: “Với đủ nhiều con mắt quan sát, mọi lỗi đều trở nên nông cạn”. Tuy nhiên, “con mắt” ở đây không chỉ tính về số lượng mà quan trọng hơn là chất lượng - những chuyên gia có chuyên môn sâu trong lĩnh vực cụ thể mới là chìa khóa. Chỉ khi có cộng đồng người dùng khổng lồ

0%