Quy trình phát triển

Race Condition: Lỗi Khó Tìm Nhất Vì Nó Chỉ Xảy Ra... Đôi Khi

Thứ năm, 01 Th10 2026 6 phút đọc 37 lượt xem

Có một loại lỗi khiến dev nào cũng từng phát điên: code chạy đúng 99 lần, đến lần thứ 100 lại cho kết quả sai, rồi chạy lại thì lại đúng tiếp. Không có stack trace rõ ràng, không tái hiện được trên máy local, nhưng lại xảy ra đều đặn trên production khi có nhiều người dùng cùng lúc. Đó chính là race condition, một trong những loại lỗi khó tìm nhất trong lập trình.

Race condition là gì

Race condition xảy ra khi kết quả của chương trình phụ thuộc vào thứ tự thực thi của nhiều tiến trình hoặc luồng (thread) chạy đồng thời, trong khi thứ tự đó lại không được đảm bảo. Nói đơn giản: hai (hoặc nhiều) đoạn code cùng "chạy đua" để truy cập và thay đổi một tài nguyên chung, và kết quả cuối cùng phụ thuộc vào việc ai "về đích" trước, điều mà máy tính không đảm bảo giống nhau ở mỗi lần chạy.

Ví dụ kinh điển nhất: một biến đếm count được hai luồng cùng thực hiện count = count + 1. Thao tác này trông như một bước, nhưng thực chất gồm ba bước ở cấp độ máy: đọc giá trị hiện tại, cộng thêm 1, rồi ghi lại. Nếu hai luồng cùng đọc giá trị count = 5 trước khi luồng nào kịp ghi lại, cả hai đều tính ra 6 và ghi đè lên nhau, kết quả cuối cùng là 6 thay vì 7.

Vì sao nó khó tìm đến vậy

Race condition phụ thuộc vào timing, tức thời điểm chính xác các luồng chạy xen kẽ nhau, thứ gần như không thể kiểm soát hay dự đoán. Trên máy lập trình viên, với lượng request thấp, hai luồng gần như không bao giờ "đụng độ" đúng lúc để lộ lỗi. Khi lên production với hàng trăm, hàng nghìn request đồng thời, xác suất va chạm tăng lên đáng kể và lỗi bắt đầu xuất hiện, nhưng không phải lần nào cũng vậy.

Điều này khiến race condition gần như vô hình với cách debug thông thường. Đặt breakpoint để xem từng bước thực chất làm chậm luồng đó lại, vô tình khiến lỗi biến mất, hiện tượng dev hay gọi đùa là "heisenbug" (lỗi biến mất khi bạn cố quan sát nó).

Một ví dụ dễ hình dung: bán vé cuối cùng

Hãy tưởng tượng hệ thống bán vé chỉ còn đúng 1 vé. Hai người dùng A và B bấm "mua vé" gần như cùng lúc. Luồng xử lý của A kiểm tra "còn vé không?" và nhận được "còn 1 vé". Ngay sau đó, luồng của B cũng kiểm tra và nhận cùng câu trả lời, vì luồng của A chưa kịp trừ kho xuống 0. Kết quả: cả A và B đều được xác nhận mua thành công, trong khi chỉ có 1 vé thực sự tồn tại. Đây là lý do nhiều hệ thống đặt vé, đặt phòng hay thương mại điện tử dễ gặp sự cố "bán vượt số lượng" trong các đợt sale lớn, lúc có nhiều người mua cùng lúc nhất.

Cách phát hiện và phòng tránh

Vì race condition không phải lúc nào cũng tái hiện được, cách hiệu quả nhất là thiết kế đúng ngay từ đầu. Một số kỹ thuật phổ biến: dùng lock hoặc mutex để chỉ một luồng được truy cập tài nguyên chung tại một thời điểm; dùng thao tác atomic (không thể bị chia nhỏ hay xen ngang) cho các phép tính đơn giản như tăng biến đếm; tận dụng transaction trong database để nhóm các thao tác đọc-ghi thành một khối; và hạn chế chia sẻ trạng thái có thể thay đổi (mutable state) giữa các luồng khi không thực sự cần.

Về kiểm thử, công cụ stress test (giả lập nhiều request đồng thời) và race detector như ThreadSanitizer hay race detector của Go hữu ích hơn nhiều so với chỉ chạy lại code và hy vọng lỗi xuất hiện. Khi review code, đoạn nào truy cập tài nguyên dùng chung (biến toàn cục, file, bản ghi database) giữa nhiều luồng hoặc nhiều request cũng đáng được soi kỹ hơn bình thường.

Race condition là lời nhắc rằng code "chạy đúng" khi test thủ công một mình không có nghĩa là logic của nó thực sự đúng. Chỉ khi nhiều người, nhiều luồng cùng chạm vào nó một lúc, sự thật mới lộ ra, và thường là vào đúng lúc tệ nhất.

Nguồn tham khảo

Sẵn sàng chuyển đổi doanh nghiệp?

Hãy thảo luận về cách chúng tôi có thể giúp bạn tận dụng AI và chuyển đổi số.

Câu hỏi thường gặp

Race condition là gì?
Là lỗi xảy ra khi kết quả chương trình phụ thuộc vào thứ tự thực thi của nhiều luồng chạy đồng thời, trong khi thứ tự đó không được đảm bảo.
Vì sao race condition khó tái hiện?
Vì nó phụ thuộc vào timing. Trên máy local ít request, các luồng hiếm khi va chạm đúng lúc; trên production nhiều request đồng thời thì lỗi mới lộ ra.
"Heisenbug" là gì?
Là lỗi biến mất khi bạn cố quan sát nó, ví dụ đặt breakpoint làm chậm luồng khiến race condition không xảy ra nữa.
Làm sao phòng tránh race condition?
Dùng lock hoặc mutex, thao tác atomic, transaction trong database, và hạn chế chia sẻ trạng thái có thể thay đổi giữa các luồng.

Chia sẻ bài viết