Quản Lý Đối Tượng Và Tài Nguyên
Trong nhiều năm sử dụng kiến trúc phần mềm C++, nỗi đau đầu lớn nhất chính là quản lý các đối tượng và tài nguyên trong bộ nhớ. Vấn đề nan giải nhất trong quản lý đối tượng là xác định thời điểm xóa đối tượng. Có lẽ nhiều người đã sớm nhận ra điều này, dẫn đến sự phát triển mạnh mẽ của công nghệ garbage collection (GC).
Khi một đối tượng bị tham chiếu bởi nhiều nơi, phương pháp phổ biến là sử dụng kỹ thuật đếm tham chiếu (reference counting). Khi bộ đếm giảm về 0, đối tượng sẽ bị xóa - một giải pháp tưởng chừng hoàn hảo. Tuy nhiên, có bao nhiêu nơi thực sự nhớ để giải phóng tham chiếu? Với sự hỗ trợ của các tính năng “syntactic sugar” trong C++, chúng ta có thể tự động hóa quy trình này. Đối với quan hệ tham chiếu dài hạn, có thể xử lý thông qua constructor và destructor; còn với tham chiếu ngắn hạn (ví dụ: một đối tượng chỉ được sử dụng trong một hàm), có thể sử dụng các đối tượng wrapper để đảm bảo giải phóng tài nguyên ngay sau khi sử dụng xong.
Dù các công cụ tiện ích này giúp giảm lỗi, nhưng chúng cũng ngầm thêm chi phí thực thi và làm phình to mã nguồn. Lúc này, việc tối ưu thiết kế hệ thống sẽ mang lại hiệu quả rõ rệt. Trong hai năm gần đây, tôi thường áp dụng mô hình quản lý xóa tập trung. Giải pháp này sử dụng một luồng chính định kỳ quét toàn bộ đối tượng và xóa những đối tượng đã được đánh dấu. Thay vì xóa trực tiếp, các thao tác thông thường chỉ thực hiện đánh dấu (mark) đối tượng cần xóa. Đặc biệt, thao tác đánh dấu là một chiều (không thể đảo ngược), tức là khi đã đánh dấu xóa thì không thể khôi phục trạng thái ban đầu. Điều này đảm bảo tính thread-safe mà không cần sử dụng khóa (lock).
Tại sao lại chọn cách này? Vì đa số các đối tượng hoặc tài nguyên trong hệ thống thường ở trạng thái chỉ đọc (read-only) sau khi khởi tạo. Các thao tác ghi (write) nếu có thường được hệ điều hành đảm bảo an toàn luồng. Ví dụ như các handle tệp tin hay socket mạng. Mỗi đối tượng đều có một thao tác đặc biệt gọi là “giải phóng” (release) - một thao tác ghi phá hủy đối tượng. Khi tách riêng thao tác giải phóng và xử lý tập trung ở môi trường an toàn, phần lớn đối tượng có thể duy trì trạng thái chỉ đọc, từ đó loại bỏ nhu cầu đồng bộ hóa luồng.
Giải pháp này còn hiệu quả trong môi trường đơn luồng. Kết hợp với kỹ thuật truy cập gián tiếp qua con trỏ, chúng ta thậm chí không cần dùng đến đếm tham chiếu. Về vấn đề singleton, việc lạm dụng static object và lazy initialization đã trở thành thói quen xấu của nhiều lập trình viên C++. Như đã biết qua bài viết “Double Checked Locking is broken”, việc quá phụ thuộc vào syntactic sugar thường dẫn đến hậu quả nghiêm trọng. Thực tế, việc thiết kế rõ ràng các giai đoạn khởi tạo và kết thúc chương trình không hề phức tạp. Việc quản lý singleton chỉ cần thực hiện đúng thời điểm và theo thứ tự hợp lý.
Trong quản lý tài nguyên toàn cục, tôi luôn ưu tiên cấu trúc dạng cây thay vì danh sách tuyến tính. Lý do là vì các thao tác quét tài nguyên (như xóa các đối tượng đã đánh dấu) sẽ hiệu quả hơn nhiều. Khi đánh dấu một nút trong cây, chúng ta đồng thời đánh dấu luôn nút cha (thông qua cơ chế đếm). Nhờ đó, từ gốc cây có thể nhanh chóng xác định các nhánh cần duyệt. Vì thao tác xóa không thường xuyên xảy ra, việc kiểm tra nút gốc trước có thể giúp bỏ qua toàn bộ quá trình duyệt không cần thiết.
Hơn nữa, các thao tác xóa hoàn toàn có thể thực hiện song song. Để không làm ảnh hưởng cấu trúc cây tài nguyên trong quá trình xóa, chúng ta có thể đánh dấu null cho các nút cần xóa, sau đó thực hiện nén cây để loại bỏ các con trỏ null. Giải pháp này đảm bảo hiệu suất tối đa, tránh tình trạng gián đoạn dịch vụ do quá trình dọn dẹp tài nguyên định kỳ.