Thiết Kế Vòng Đệm Vòng Không Khóa Cho Hệ Thống Log
Trong hai ngày qua, tôi đã tiến hành cải tiến module ghi log. Chúng tôi cần xây dựng một module hỗ trợ ghi log đồng thời từ nhiều nguồn, với cấu trúc gồm nhiều producer và một consumer duy nhất. Consumer này chạy trong một luồng chuyên dụng để lưu trữ log xuống ổ đĩa.
Đặc điểm quan trọng là đa số producer log đều nằm trong các callback của thư viện bên thứ ba như bgfx. Nếu quá trình ghi log không đủ nhanh sẽ gây nghẽn luồng rendering. Các callback này yêu cầu phải tự đảm bảo an toàn luồng (thread-safe), đặc biệt bgfx hỗ trợ rendering đa luồng nên các callback ghi log có thể được kích hoạt từ nhiều luồng khác nhau.
Trước đây khi triển khai luabinding cho bgfx, tôi đã xây dựng một hàng đợi MPSC đơn giản. Hàm get_log
đóng vai trò consumer duy nhất, có nhiệm vụ thu thập toàn bộ dữ liệu log trong hàng đợi và trả về máy ảo Lua. Phiên bản cũ sử dụng cơ chế spin_lock
, nhưng hiện tại tôi muốn phát triển một phiên bản không khóa (lock-free) tổng quát hơn.
Mô hình hai vòng đệm vòng (Double Ringbuffer)
Trong yêu cầu của hệ thống, việc cho phép mất một số log không gây ảnh hưởng nghiêm trọng. Tôi quyết định sử dụng một vòng đệm cố định (ringbuffer) để thu thập log từ các luồng khác nhau, sau đó định kỳ (thường mỗi frame rendering) consumer sẽ lấy dữ liệu. Với tần suất lấy dữ liệu cao và kích thước vòng đệm phù hợp (ví dụ 4096 mục), cấu trúc đơn giản này có thể giải quyết vấn đề hiệu quả.
Hệ thống cần hai vòng đệm riêng biệt:
- Ringbuffer Meta: Lưu trữ thông tin metadata (offset và size) của từng log
- Ringbuffer Data: Chứa nội dung văn bản thực tế của log
Vòng đệm Metadata (4096 mục)
- Sử dụng hai biến 64-bit:
head
(cho consumer) vàtail
(cho producer, cần atomic) - Quy trình ghi dữ liệu:
index = fetch_and_add(tail, 1)
- Ghi metadata (offset + size) vào
buffer[index % 4096]
Đặc biệt, thứ tự ghi metadata phải tuân thủ: ghi offset trước, size sau để đảm bảo tính nhất quán khi consumer đọc.
Vòng đệm Data (64KB)
- Dùng biến atomic 64-bit
ptr
để quản lý vị trí ghi - Quy trình ghi dữ liệu:
offset = fetch_and_add(ptr, size)
- Sao chép chuỗi log vào
buffer + offset % 64KB
(xử lý wrap-around khi cần)
Consumer kiểm tra tính hợp lệ của dữ liệu bằng cách so sánh offset + 64KB
với ptr
. Nếu offset + 64KB < ptr
, dữ liệu đã bị ghi đè và không còn hiệu lực.
Quy trình xử lý của Consumer
- Kiểm tra
head == tail
: hàng đợi rỗng - Nếu
meta.size < 0
: dữ liệu chưa sẵn sàng - Tăng
head
và lấyindex
- Dựa vào
offset
vàsize
từ metadata để truy xuất nội dung từ ringbuffer data - Đặt
meta.size = -1
để đánh dấu slot đã được đọc, đảm bảo producer có thể phát hiện trạng thái ghi dở dang
Triển khai thực nghiệm
Tôi đã viết một phiên bản thử nghiệm đơn giản:
Lưu ý: Mã nguồn này chưa qua kiểm thử kỹ lưỡng, đặc biệt là với các cấu trúc không khóa vốn rất dễ gặp lỗi. Đây chỉ là tài liệu tham khảo để minh họa ý tưởng. Mặc dù thiết kế khá đơn giản, nhưng nếu có lỗi thì cũng dễ phát hiện và sửa chữa.