Duyệt Tập Hợp Một Cách an Toàn
Việc đặt các phần tử đồng nhất vào một cấu trúc dữ liệu, sau đó dùng con trỏ duyệt (iterator) để xử lý từng phần tử là một yêu cầu phổ biến trong lập trình. Tuy nhiên, quá trình này tiềm ẩn nhiều rủi ro gây ra lỗi hệ thống. Vấn đề nằm ở tính nguyên tử (atomicity) — khi vừa duyệt vừa thay đổi cấu trúc dữ liệu, hai thao tác này gần như không thể xảy ra đồng thời một cách an toàn.
Hãy tưởng tượng bạn đang duyệt một danh sách các hàm callback. Trong lúc vòng lặp đang chạy, một hàm nào đó được gọi và bất ngờ thêm/xóa phần tử khỏi danh sách. Kết quả? Con trỏ duyệt có thể bị trôi nổi, truy cập vùng nhớ không tồn tại, hoặc lặp vô hạn. Đây là lỗi kinh điển trong thiết kế hệ thống đồng thời.
Giải pháp thông minh: Phân tầng trạng thái duyệt
Thay vì sử dụng trực tiếp các cấu trúc như std::vector
hay std::list
, ta xây dựng một mô hình duyệt đa cấp với các nguyên tắc sau:
-
Tách biệt giữa thay đổi và duyệt
- Dùng mảng động để lưu trữ các phần tử, nhưng chỉ cho phép chèn vào cuối mảng.
- Khi cần xóa phần tử, không xóa vật lý mà đánh dấu (mark) phần tử đó cần bỏ qua.
-
Ngăn xếp trạng thái duyệt (Iteration Stack)
- Mỗi lần bắt đầu duyệt, hệ thống tạo một đối tượng trạng thái chứa:
- Vị trí hiện tại trong mảng
- Danh sách các phần tử hợp lệ cần xử lý
- Nếu có yêu cầu duyệt lồng nhau (ví dụ: callback gọi đệ quy), mỗi yêu cầu sẽ nhận được trạng thái độc lập mà không làm rối loạn các vòng lặp đang chờ.
- Mỗi lần bắt đầu duyệt, hệ thống tạo một đối tượng trạng thái chứa:
-
Dọn dẹp sau khi duyệt
Khi không còn vòng lặp nào đang chạy, hệ thống quét toàn bộ mảng để:- Xóa vĩnh viễn các phần tử bị đánh dấu
- Nén mảng, loại bỏ khoảng trống (compaction)
Triển khai trong thực tế
Với Lua:
|
|
Với C++:
Cần sử dụng shared_ptr
để quản lý vòng đời phần tử, kết hợp vector<bool>
để đánh dấu hiệu quả. Đảm bảo rằng các hàm callback không trực tiếp thay đổi cấu trúc dữ liệu gốc mà chỉ gửi yêu cầu vào một hàng đợi thao tác (operation queue).
Ứng dụng thực tiễn
- Quản lý sự kiện trong game engine: Khi một sự kiện “PlayerDead” xảy ra, tất cả hệ thống lắng nghe sẽ được thông báo, có thể tự hủy hoặc tạo thực thể mới mà không làm crash engine.
- Thu gom rác (Garbage Collection): Đánh dấu các đối tượng không còn tham chiếu trong lúc chương trình chạy, dọn dẹp định kỳ để tối ưu bộ nhớ.
Lưu ý thiết kế
- Hiệu năng vs An toàn: Việc đánh dấu và nén mảng tạo ra overhead. Cần cân bằng giữa tần suất gọi các hàm dọn dẹp và độ phức tạp của hệ thống.
- Deadlock tiềm ẩn: Nếu dùng cơ chế đa luồng, cần đồng bộ hóa bằng mutex khi truy cập cấu trúc dữ liệu chung.
Mô hình này không chỉ giải quyết bài toán duyệt an toàn, mà còn mở ra hướng thiết kế cho các hệ thống phản ứng (reactive systems) trong môi trường đa luồng hoặc bất đồng bộ.