Hàm Gọi Lại Ghi Log Đảm Bảo an Toàn Luồng - nói dối e blog

Hàm Gọi Lại Ghi Log Đảm Bảo an Toàn Luồng

Trong quá trình phát triển động cơ đồ họa 3D, nhóm phát triển phát hiện ra một điểm đặc biệt khi sử dụng thư viện API render bgfx. Hàm callback ghi log do bgfx cung cấp yêu cầu người dùng tự đảm bảo tính an toàn đa luồng. Điều này có nghĩa là bgfx có thể kích hoạt hàm callback này từ nhiều luồng xử lý đồng thời (đặc biệt khi kích hoạt cơ chế render đa luồng).

Trong trường hợp đơn giản chỉ ghi log ra file (như stdout tiêu chuẩn), tính an toàn luồng thường được CRT tự động xử lý. Tuy nhiên nếu muốn xử lý phức tạp hơn như đẩy chuỗi log vào máy ảo Lua, người lập trình bắt buộc phải tự xây dựng cơ chế đồng bộ hóa.

Giải pháp hiện tại của chúng tôi là thiết kế một vùng đệm log an toàn đa luồng dùng chung. Hàm callback sẽ ghi dữ liệu log vào vùng đệm này, sau đó máy ảo Lua sẽ sao chép dữ liệu từ vùng đệm sang không gian bộ nhớ của mình vào mỗi frame.

Trong quá trình xây dựng module vùng đệm log an toàn luồng, tôi đã phát triển một phương pháp tối ưu đặc biệt hiệu quả.

Đối với đa số ứng dụng đồ họa 3D, số lượng luồng xử lý thường không quá lớn (thường chỉ có luồng chính và luồng render). Giải pháp đơn giản nhất là sử dụng biến TLS (Thread Local Storage) để tạo vùng đệm riêng biệt cho từng luồng. Tuy nhiên do muốn giữ cho thư viện bgfx-Lua được gọn nhẹ, tôi đã chọn cách tiếp cận khác nhưng cùng nguyên lý.

Sử dụng cấu trúc dữ liệu không khóa (lock-free) cũng là một phương án khả thi. Tuy nhiên việc triển khai các cấu trúc này đòi hỏi độ chính xác cực cao, dễ phát sinh lỗi khó phát hiện, khiến tôi phải từ bỏ phương án này.

Giải pháp cuối cùng được áp dụng như sau:

  1. Tạo ra N vùng đệm log độc lập, mỗi vùng có khóa bảo vệ riêng.
  2. Khi cần ghi log, tiến hành lần lượt kiểm tra khóa của các vùng đệm. Nếu khóa thành công, ghi dữ liệu vào vùng đệm tương ứng. Nếu khóa thất bại, chuyển sang vùng đệm kế tiếp.
  3. Chỉ khi thử hết tất cả các vùng đệm mà đều thất bại mới thực hiện thao tác khóa chờ (blocking).

Với số lượng luồng hạn chế trong thực tế, khả năng phải chờ khóa trực tiếp rất thấp. Mỗi lần ghi log sẽ tự động chọn được vùng đệm khả dụng.

Về phía đọc log:

  1. Tiến hành tương tự kiểm tra từng vùng đệm để khóa và đọc.
  2. Tuy nhiên nếu gặp vùng đệm bị khóa, sẽ bỏ qua mà không chờ.
  3. Do thao tác đọc luôn diễn ra trên luồng chính (nơi chứa máy ảo Lua), đa số log từ các API của bgfx cũng sinh ra ở luồng này nên khả năng đọc thành công rất cao.
  4. Các log từ luồng render nếu bỏ sót ở lần đọc này, sẽ được xử lý ở frame kế tiếp.

Tuy phương án này về mặt lý thuyết tồn tại nguy cơ “đói tài nguyên” (starvation), nhưng trong thực tế khả năng này gần như không xảy ra. Hơn nữa, các log ở client không cần độ chính xác tuyệt đối. Mỗi vùng đệm log được thiết kế là một buffer vòng 64KB cố định. Khi buffer đầy, hệ thống sẽ tạm dừng ghi mà không thực hiện khóa, điều này giúp loại bỏ hoàn toàn nguy cơ chết khóa (deadlock) trong quá trình đọc.

Giải pháp sáng tạo này được xây dựng trên ba tiền đề quan trọng:

  • Cho phép mất mát log trong tình huống cực đoan
  • Không cần đảm bảo thứ tự chính xác giữa các luồng log
  • Số lượng luồng ghi log là cố định và nhỏ

Ưu điểm nổi bật là giải quyết triệt để vấn đề cạnh tranh khóa giữa luồng chính và luồng render, tránh làm giảm hiệu suất song song của hệ thống. Đồng thời phương án này có độ phức tạp vừa phải, dễ kiểm thử và bảo trì, phù hợp với yêu cầu thực tế của dự án.

0%